以前项目中权限认证没有使用安全框架,都是在自定义filter中判断是否登录以及用户是否有操作权限的。
最近开了新项目,搭架子时,想到使用安全框架来解决认证问题,spring security太过庞大,我们的项目不大,所以决定采用Shiro
什么是Shiro
Apache Shiro 是一个强大灵活的开源安全框架,可以完全处理身份验证、授权、加密和会话管理。
Realm是Shiro的核心组建,也一样是两步走,认证和授权,在Realm中的表现为以下两个方法。
- 认证:doGetAuthenticationInfo,核心作用判断登录信息是否正确
- 授权:doGetAuthorizationInfo,核心作用是获取用户的权限字符串,用于后续的判断
Shiro过滤器
当 Shiro 被运用到 web 项目时,Shiro 会自动创建一些默认的过滤器对客户端请求进行过滤。以下是 Shiro 提供的部分过滤器:
过滤器 | 描述 |
---|---|
anon | 表示可以匿名使用 |
authc | 表示需要认证(登录)才能使用 |
authcBasic | 表示httpBasic认证 |
perms | 当有多个参数时必须每个参数都通过才通过 perms[“user:add:”] |
port | port[8081] 跳转到schemal://serverName:8081?queryString |
rest | 权限 |
roles | 角色 |
ssl | 表示安全的url请求 |
user | 表示必须存在用户,当登入操作时不做检查 |
为什么选择shiro
- 简单性,Shiro 在使用上较 Spring Security 更简单,更容易理解。
- 灵活性,Shiro 可运行在 Web、EJB、IoC、Google App Engine 等任何应用环境,却不依赖这些环境。而 Spring Security 只能与 Spring 一起集成使用。
- 可插拔,Shiro 干净的 API 和设计模式使它可以方便地与许多的其它框架和应用进行集成。Shiro 可以与诸如 Spring、Grails、Wicket、Tapestry、Mule、Apache Camel、Vaadin 这类第三方框架无缝集成。Spring Security 在这方面就显得有些捉衿见肘。
spring boot整合shiro
添加maven依赖
在项目中引入shiro非常简单,我们只需要引入 shiro-pring 就可以了
<!-- SECURITY begin -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<!-- SECURITY end -->
shiro自定义认证token
AuthenticationToken 用于收集用户提交的身份(如用户名)及凭据(如密码)。Shiro会调用CredentialsMatcher对象的doCredentialsMatch方法对AuthenticationInfo对象和AuthenticationToken进行匹配。匹配成功则表示主体(Subject)认证成功,否则表示认证失败。
Shiro 仅提供了一个可以直接使用的 UsernamePasswordToken,用于实现基于用户名/密码主体(Subject)身份认证。UsernamePasswordToken实现了 RememberMeAuthenticationToken 和 HostAuthenticationToken,可以实现“记住我”及“主机验证”的支持。
我们的业务逻辑是每次调用接口,不使用session存储登录状态,使用在head里面存token的方式,所以不使用session,并不需要用户密码认证。
自定义token如下:
/**
* Created by Youdmeng on 2020/6/24 0024.
*/
public class YtoooToken implements AuthenticationToken {
private String token;
public YtoooToken(String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
shiro自定义Realm
Realm是shiro的核心组件,主要处理两大功能:
- 认证 我们接收filter传过来的token,并认证login操作的token
- 授权 获取到登录用户信息,并取得用户的权限存入roles,以便后期对接口进行操作权限验证
@Slf4j
public class UserRealm extends AuthorizingRealm {
@Autowired
private JedisClusterClient jedis;
/**
* 大坑!,必须重写此方法,不然Shiro会报错
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof YtoooToken;
}
/**
* 授权
*
* @param principals
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
log.info("Shiro权限配置");
String token = principals.toString();
UserDetailVO userDetailVO = JSON.parseObject(jedis.get(token), UserDetailVO.class);
Set<String> roles = new HashSet<>();
roles.add(userDetailVO.getAuthType() + "");
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.setRoles(roles);
return info;
}
/**
* 认证
*
* @param token
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
log.info("Shiror认证");
YtoooToken usToken = (YtoooToken) token;
//获取用户的输入的账号.
String sid = (String) usToken.getCredentials();
if (StringUtils.isBlank(sid)) {
return null;
}
log.info("sid: " + sid);
return new SimpleAccount(sid, sid, "userRealm");
}
}
shiro自定义拦截器
自定义shiro拦截器来控制指定请求的访问权限,并登录shiro以便认证
我们自定义shiro拦截器主要使用其中的两个方法:
- isAccessAllowed() 判断是否可以登录到系统
- onAccessDenied() 当isAccessAllowed()返回false时,登录被拒绝,进入此接口进行异常处理
/**
* Created by Youdmeng on 2020/6/24 0024.
*/
@Slf4j
public class TokenFilter extends FormAuthenticationFilter {
private String errorCode;
private String errorMsg;
private static JedisClusterClient jedis = JedisClusterClient.getInstance();
/**
* 如果在这里返回了false,请求onAccessDenied()
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String sid = httpServletRequest.getHeader("sid");
if (StringUtils.isBlank(sid)) {
this.errorCode = ResponseEnum.TOKEN_UNAVAILABLE.getCode();
this.errorMsg = ResponseEnum.TOKEN_UNAVAILABLE.getMessage();
return false;
}
log.info("sid: " + sid);
UserDetailVO userInfo = null;
try {
userInfo = JSON.parseObject(jedis.get(sid), UserDetailVO.class);
} catch (Exception e) {
this.errorCode = ResponseEnum.TOKEN_EXPIRE.getCode();
this.errorMsg = ResponseEnum.TOKEN_EXPIRE.getMessage();
return false;
}
if (userInfo == null) {
this.errorCode = ResponseEnum.TOKEN_EXPIRE.getCode();
this.errorMsg = ResponseEnum.TOKEN_EXPIRE.getMessage();
return false;
}
//刷新超时时间
jedis.expire(sid, 30 * 60); //30分钟过期
YtoooToken token = new YtoooToken(sid);
// 提交给realm进行登入,如果错误他会抛出异常并被捕获
getSubject(request, response).login(token);
// 如果没有抛出异常则代表登入成功,返回true
return true;
}
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) {
ResponseMessage result = Result.error(this.errorCode,this.errorMsg);
String reponseJson = (new Gson()).toJson(result);
response.setContentType("application/json; charset=utf-8");
response.setCharacterEncoding("utf-8");
ServletOutputStream outputStream = null;
try {
outputStream = response.getOutputStream();
outputStream.write(reponseJson.getBytes());
} catch (IOException e) {
log.error("权限校验异常",e);
} finally {
if (outputStream != null){
try {
outputStream.flush();
outputStream.close();
} catch (IOException e) {
log.error("权限校验,关闭连接异常",e);
}
}
}
return false;
}
}
配置ShiroConfig
springboot中,组件通过@Bean的方式交由spring统一管理,在这里需要配置 securityManager,shiroFilter,AuthorizationAttributeSourceAdvisor
注入realm
@Bean
public UserRealm userRealm() {
UserRealm userRealm = new UserRealm();
return userRealm;
}
注入 securityManager
@Bean("securityManager")
public DefaultWebSecurityManager getManager(UserRealm realm) {
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
// 使用自己的realm
manager.setRealm(realm);
/*
* 关闭shiro自带的session,详情见文档
* http://shiro.apache.org/session-management.html#SessionManagement-StatelessApplications%28Sessionless%29
*/
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
manager.setSubjectDAO(subjectDAO);
return manager;
}
注入 shiroFilter
此处将自定义过滤器添加到shiro中,并配置具体哪些路径,执行shiro的那些过滤规则
@Bean("shiroFilter")
public ShiroFilterFactoryBean factory(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
// 添加自己的过滤器并且取名为token
Map<String, Filter> filterMap = new HashMap<>();
filterMap.put("token", new TokenFilter());
factoryBean.setFilters(filterMap);
factoryBean.setSecurityManager(securityManager);
/*
* 自定义url规则
* http://shiro.apache.org/web.html#urls-
*/
Map<String, String> filterRuleMap = new HashMap<>();
//swagger
filterRuleMap.put("/swagger-ui.html", "anon");
filterRuleMap.put("/**/*.js", "anon");
filterRuleMap.put("/**/*.png", "anon");
filterRuleMap.put("/**/*.ico", "anon");
filterRuleMap.put("/**/*.css", "anon");
filterRuleMap.put("/**/ui/**", "anon");
filterRuleMap.put("/**/swagger-resources/**", "anon");
filterRuleMap.put("/**/api-docs/**", "anon");
//swagger
//登录
filterRuleMap.put("/login/login", "anon");
filterRuleMap.put("/login/verifyCode", "anon");
// 所有请求通过我们自己的JWT Filter
filterRuleMap.put("/**", "token");
factoryBean.setFilterChainDefinitionMap(filterRuleMap);
return factoryBean;
配置DefaultAdvisorAutoProxyCreator
解决 在@Controller注解的类的方法中加入@RequiresRole等shiro注解,会导致该方法无法映射请求,导致返回404。
@Bean
public static DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator=new DefaultAdvisorAutoProxyCreator();
/**
* setUsePrefix(false)用于解决一个奇怪的bug。在引入spring aop的情况下。
* 在@Controller注解的类的方法中加入@RequiresRole等shiro注解,会导致该方法无法映射请求,导致返回404。
* 加入这项配置能解决这个bug
*/
defaultAdvisorAutoProxyCreator.setUsePrefix(true);
return defaultAdvisorAutoProxyCreator;
}
配置 AuthorizationAttributeSourceAdvisor 使doGetAuthorizationInfo()Shiro权限配置生效
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
在接口中控制权限
使用RequiresRoles注解来配置该接口需要的权限
当配置logical = Logical.OR时,登录这配置的权限在1,2,3中任意一个,既可以成功访问接口
@ApiOperation("任务调度")
@PostMapping("/dispatch")
@RequiresRoles(value = { "1", "2", "3" }, logical = Logical.OR)
public ResponseMessage dispatch(@RequestBody @Valid DispatchVO dispatchVO) {
log.info("任务调度开始 入参:" + JSON.toJSONString(dispatchVO));
try {
service.dispatch(dispatchVO);
return Result.success(ResponseEnum.SUCCESS.getCode(), ResponseEnum.SUCCESS.getMessage());
} catch (RuntimeException e) {
log.error("任务调度失败", e);
return Result.error(ResponseEnum.ERROR.getCode(), e.getMessage());
} catch (Exception e) {
log.error("任务调度失败", e);
return Result.error(ResponseEnum.ERROR.getCode(), ResponseEnum.ERROR.getMessage());
}
}
统一的异常处理
配置全局异常处理
@ControllerAdvice
@Order(value=1)
public class ShiroExceptionAdvice {
private static final Logger logger = LoggerFactory.getLogger(ShiroExceptionAdvice.class);
@ResponseStatus(HttpStatus.UNAUTHORIZED)
@ExceptionHandler({AuthenticationException.class, UnknownAccountException.class,
UnauthenticatedException.class, IncorrectCredentialsException.class})
@ResponseBody
public ResponseMessage unauthorized(Exception exception) {
logger.warn(exception.getMessage(), exception);
logger.info("catch UnknownAccountException");
return Result.error(ResponseEnum.NOT_AUTHORIZED.getCode(), ResponseEnum.NOT_AUTHORIZED.getMessage());
}
@ResponseStatus(HttpStatus.UNAUTHORIZED)
@ExceptionHandler(UnauthorizedException.class)
@ResponseBody
public ResponseMessage unauthorized1(UnauthorizedException exception) {
logger.warn(exception.getMessage(), exception);
return Result.error(ResponseEnum.NOT_AUTHORIZED.getCode(), ResponseEnum.NOT_AUTHORIZED.getMessage());
}
}
上面使用的redis工具
@Bean
@DependsOn("ConfigUtil")
public JedisClusterClient getClient() {
ml.ytooo.redis.RedisProperties.expireSeconds = redisProperties.getExpireSeconds();
ml.ytooo.redis.RedisProperties.clusterNodes = redisProperties.getClusterNodes();
ml.ytooo.redis.RedisProperties.connectionTimeout = redisProperties.getConnectionTimeout();
ml.ytooo.redis.RedisProperties.soTimeout = redisProperties.getSoTimeout();
ml.ytooo.redis.RedisProperties.maxAttempts = redisProperties.getMaxAttempts();
if (StringUtils.isNotBlank(redisProperties.password)) {
ml.ytooo.redis.RedisProperties.password = redisProperties.password;
}else {
ml.ytooo.redis.RedisProperties.password = null;
}
return JedisClusterClient.getInstance();
}
@Data
@Component
@ConfigurationProperties(prefix = "redis.cache")
public class RedisProperties {
private int expireSeconds;
private String clusterNodes;
private int connectionTimeout;
private String password;
private int soTimeout;
private int maxAttempts;
}
依赖工具集:
<dependency>
<groupId>ml.ytooo</groupId>
<artifactId>ytooo-util</artifactId>
<version>3.7.0</version>
</dependency>
收工
更多好玩好看的内容,欢迎到我的博客交流,共同进步 WaterMin
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。