以前项目中权限认证没有使用安全框架,都是在自定义filter中判断是否登录以及用户是否有操作权限的。
Apache Shiro 是一个强大灵活的开源安全框架,可以完全处理身份验证、授权、加密和会话管理。
Realm是Shiro的核心组建,也一样是两步走,认证和授权,在Realm中的表现为以下两个方法。
当 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-pring 就可以了
<!-- SECURITY begin --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.4.0</version> </dependency> <!-- SECURITY end -->
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; } }
Realm是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拦截器主要使用其中的两个方法:
/** * 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; } }
springboot中,组件通过@Bean的方式交由spring统一管理,在这里需要配置 securityManager,shiroFilter,AuthorizationAttributeSourceAdvisor
@Bean public UserRealm userRealm() { UserRealm userRealm = new UserRealm(); return userRealm; }
@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; }
此处将自定义过滤器添加到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;
解决 在@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; }
@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()); } }
@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