背景交代
以前项目中权限认证没有使用安全框架,都是在自定义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-spring 就可以了
<!-- 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时,登录被拒绝,进入此接口进行异常处理
@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>
关注我!Java从此不迷路!
作者:我是大月饼
链接:
https://blog.csdn.net/Youdmeng/article/details/107179579