以前项目中权限认证没有使用安全框架,都是在自定义 filter 中判断是否登录以及用户是否有操作权限的。 最近开了新项目,搭架子时,想到使用安全框架来解决认证问题,spring security 太过庞大,我们的项目不大,所以决定采用 Shiro 什么是 ShiroApache Shiro 是一个强大灵活的开源安全框架,可以完全处理身份验证、授权、加密和会话管理。 Realm 是 Shiro 的核心组建,也一样是两步走,认证和授权,在 Realm 中的表现为以下两个方法。 Shiro 过滤器当 Shiro 被运用到 web 项目时,Shiro 会自动创建一些默认的过滤器对客户端请求进行过滤。以下是 Shiro 提供的部分过滤器: 
为什么选择 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 自定义认证 tokenAuthenticationToken 用于收集用户提交的身份(如用户名)及凭据(如密码)。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 自定义 RealmRealm 是 shiro 的核心组件,主要处理两大功能: @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 拦截器主要使用其中的两个方法: /** * 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; } }
配置 ShiroConfigspringboot 中,组件通过 @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./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./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>
|