之前項目中權限認證沒有使用安全框架,都是在自定義filter中判斷是否登陸以及用戶是否有操做權限的。
最近開了新項目,搭架子時,想到使用安全框架來解決認證問題,spring security太過龐大,咱們的項目不大,因此決定採用Shiro
Apache Shiro 是一個強大靈活的開源安全框架,能夠徹底處理身份驗證、受權、加密和會話管理。css
Realm是Shiro的核心組建,也同樣是兩步走,認證和受權,在Realm中的表現爲如下兩個方法。html
當 Shiro 被運用到 web 項目時,Shiro 會自動建立一些默認的過濾器對客戶端請求進行過濾。如下是 Shiro 提供的部分過濾器:java
過濾器 | 描述 |
---|---|
anon | 表示能夠匿名使用 |
authc | 表示須要認證(登陸)才能使用 |
authcBasic | 表示httpBasic認證 |
perms | 當有多個參數時必須每一個參數都經過才經過 perms[「user:add:」] |
port | port[8081] 跳轉到schemal://serverName:8081?queryString |
rest | 權限 |
roles | 角色 |
ssl | 表示安全的url請求 |
user | 表示必須存在用戶,當登入操做時不作檢查 |
在項目中引入shiro很是簡單,咱們只須要引入 shiro-pring 就能夠了web
<!-- 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)認證成功,不然表示認證失敗。redis
Shiro 僅提供了一個能夠直接使用的 UsernamePasswordToken,用於實現基於用戶名/密碼主體(Subject)身份認證。UsernamePasswordToken實現了 RememberMeAuthenticationToken 和 HostAuthenticationToken,能夠實現「記住我」及「主機驗證」的支持。spring
咱們的業務邏輯是每次調用接口,不使用session存儲登陸狀態,使用在head裏面存token的方式,因此不使用session,並不須要用戶密碼認證。
自定義token以下:apache
/** * 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的核心組件,主要處理兩大功能:json
@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攔截器主要使用其中的兩個方法:api
/** * 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