一、整合流程邏輯
二、整合步驟
1. 匯入shiro-redis的starter包:還有jwt的工具包,以及為了簡化開發,我引入了hutool工具包。
<!--shiro-redis整合-->
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis-spring-boot-starter</artifactId>
<version>3.2.1</version>
</dependency>
<!-- hutool工具類-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.3.3</version>
</dependency>
<!-- jwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
2. 編寫配置
- 引入RedisSessionDAO和RedisCacheManager,實現將shiro許可權資料和會話資訊儲存到redis中,實現會話共享。
- 重寫 shiro中的SessionManager和DefaultWebSecurityManager,同時在重寫的DefaultWebSecurityManager中關閉shiro自 帶的session,需要設定位false,這樣使用者將不能通過session方式登陸shiro。後面採用jwt憑證登陸。
- 重寫 shiro的ShiroFilterChainDefinition 註冊自己的過濾器。我們將不再通過編碼方式攔截訪問路徑,而是所有路徑通過自己註冊的JwtFilter過濾器,然後判斷是否有jwt憑證,有則登陸,無則跳過,跳過之後,有shiro的許可權註解進行攔截,eg:@RequiredAuthentication,這樣控制許可權訪問。
@Configuration
public class ShiroConfig {
@Autowired
JwtFilter jwtFilter;
/**
* session域管理
* @param redisSessionDAO
* @return
*/
@Bean
public SessionManager sessionManager(RedisSessionDAO redisSessionDAO) {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
// inject redisSessionDAO
sessionManager.setSessionDAO(redisSessionDAO);
return sessionManager;
}
/**
* 重寫shiro的安全管理容器,
* @param accountRealm
* @param sessionManager
* @param redisCacheManager
* @return
*/
@Bean
public SessionsSecurityManager securityManager(AccountRealm accountRealm, SessionManager sessionManager, RedisCacheManager redisCacheManager) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(accountRealm);
//inject sessionManager
securityManager.setSessionManager(sessionManager);
// inject redisCacheManager
securityManager.setCacheManager(redisCacheManager);
return securityManager;
}
/**
* 定義過濾器
* @return
*/
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition(){
// 申請一個預設的過濾器鏈
DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
Map<String,String> filterMap = new LinkedHashMap<>();
//新增一個jwt過濾器到過濾器鏈中
filterMap.put("/**","jwt");
chainDefinition.addPathDefinitions(filterMap);
return chainDefinition;
}
/**
* 過濾器工廠業務
* @param securityManager shiro中的安全管理
* @param shiroFilterChainDefinition
* @return
*/
@Bean("shiroFilterFactoryBean")
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager,
ShiroFilterChainDefinition shiroFilterChainDefinition){
/*shiro過濾器bean物件*/
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager);
// 需要新增的過濾規則
Map<String,Filter> filters = new HashMap<>();
filters.put("jwt",jwtFilter);
shiroFilter.setFilters(filters);
Map<String,String> filterMap = shiroFilterChainDefinition.getFilterChainMap();
shiroFilter.setFilterChainDefinitionMap(filterMap);
return shiroFilter;
}
}
3. 編寫realm
AccountRealm shiiro進行登陸或者許可權校驗的邏輯。
需要重寫三個方法。
- supports:為了使realm支援jwt的憑證校驗
- doGetAuthorizationInfo:許可權校驗
doGetAuthenticationInfo:登陸認證校驗
@Slf4j @Component public class AccountRealm extends AuthorizingRealm{ @Autowired JwtUtils jwtUtils; @Autowired UserService userService; /** * 判斷是否為jwt的token * @param token * @return */ @Override public boolean supports(AuthenticationToken token) { return token instanceof JwtToken; } /** * 許可權驗證 * @param principalCollection * @return */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { return null; } /** * 登陸認證 * @param authenticationToken * @return * @throws AuthenticationException */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { // 將傳入的AuthenticationToken強轉JwtToken JwtToken jwtToken = (JwtToken) authenticationToken; // 獲取jwtToken中的userId String userId = jwtUtils.getClaimByToken((String) jwtToken.getPrincipal()).getSubject(); // 根據jwtToken中的userId查詢資料庫 User user = userService.getById(Long.valueOf(userId)); if(user == null){ throw new UnknownAccountException("賬戶不存在!"); } if(user.getStatus() == -1){ throw new LockedAccountException("賬戶已被鎖定!"); } // 將可以顯示的資訊放在該載體中,對於密碼這種隱祕資訊不需要放在該載體中 AccountProfile accountProfile = new AccountProfile(); BeanUtils.copyProperties(user,accountProfile); log.info("jwt------------->{}",jwtToken); // 將token中使用者的基本資訊返回給shiro return new SimpleAuthenticationInfo(accountProfile,jwtToken.getCredentials(),getName()); } }
主要配置doGetAuthenticationInfo登陸認證這個方法,通過jwt憑證獲取使用者資訊,判斷使用者的狀態,最後異常就丟擲相應的異常資訊。
4.編寫JwtToken
shiro預設supports支援的是UsernamePasswordToken,而我們採用jwt的方式,故需要定義一個JwtToken來重寫該token。
public class JwtToken implements AuthenticationToken{ private String token; public JwtToken(String token){ this.token = token; } @Override public Object getPrincipal() { return token; } @Override public Object getCredentials() { return token; } }
5.編寫JwtUtils生成和校驗jwt的工具類
有些jwt相關的金鑰資訊是從專案的配置檔案中獲取的。
@Component @ConfigurationProperties(prefix = "mt.vuemtblog.jwt") public class JwtUtils { private String secret; private long expire; private String header; /** * 生成jwt token * @param userId * @return */ public static String generateToken(long userId){ return null; } /** * 獲取jwt的資訊 * @param token * @return */ public static Claims getClaimByToken(String token){ return null; } /** * 驗證token是否過期 * @param expiration * @return true 過期 */ public static boolean isTokenExpired(Date expiration){ return expiration.before(new Date()); } }
6.編寫登陸成功返回使用者資訊的載體AccountProfile
@Data public class AccountProfile implements Serializable { private Long id; private String username; private String avatar; }
7. 全域性配置基本資訊
shiro-redis: enabled: true redis-manger: host:127.0.0.1:6379 mt: vuemtblog: jwt: #加密金鑰 secret:f4e2e52034348f86b67cde581c0f9eb5 # token 有效時長 7天 單位秒 expire:604800 # 設定token在header中的鍵值 header:authorization
8. 若專案使用spring-boot-devtools,需要新增一個配置檔案,
在resources目錄下新建META-INF,然後新建spring-devtools.properties,這樣熱重啟就不會報錯。
restart.include.shiro-redis=/shiro-[\\w-\\.]+jar
9. 編寫自定義的JwtFileter過濾器
這裡我們繼承的是Shiro內建的AuthenticatingFilter,一個可以內建了可以自動登入方法的的過濾器,有些同學繼承BasicHttpAuthenticationFilter也是可以的。
我們需要重寫幾個方法:
- createToken:實現登入,我們需要生成我們自定義支援的JwtToken
- onAccessDenied:攔截校驗,當頭部沒有Authorization時候,我們直接通過,不需要自動登入;當帶有的時候,首先我們校驗jwt的有效性,沒問題我們就直接執行executeLogin方法實現自動登入
- onLoginFailure:登入異常時候進入的方法,我們直接把異常資訊封裝然後丟擲
preHandle:攔截器的前置攔截,因為我們是前後端分析專案,專案中除了需要跨域全域性配置之外,我們再攔截器中也需要提供跨域支援。這樣,攔截器才不會在進入Controller之前就被限制了。
@Component public class JwtFilter extends AuthenticatingFilter{ @Autowired JwtUtils jwtUtils; /** * 實現登陸,生成自定義的JwtToken * @param servletRequest * @param servletResponse * @return * @throws Exception */ @Override protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception { HttpServletRequest request = (HttpServletRequest)servletRequest; String jwt = request.getHeader("Authorization"); if(StringUtils.isEmpty(jwt)){ return null; } return new JwtToken(jwt); } /** * 攔截校驗 * @description 當頭部沒有Authorization,直接通過,不需要自動登陸。 * 當帶有Authorization時,需要先校驗jwt的時效性,沒問題直接執行executeLogin實現自動登陸,將token委託給shiro。 * @param servletRequest * @param servletResponse * @return * @throws Exception */ @Override protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception { HttpServletRequest request = (HttpServletRequest) servletRequest; // 獲取使用者請求頭中的token String token = request.getHeader("Authorization"); if (StringUtils.isEmpty(token)) {// 沒有token return true; } else { // 校驗jwt Claims claim = jwtUtils.getClaimByToken(token); // tonken為空或者時間過期 if (claim == null || jwtUtils.isTokenExpired((claim.getExpiration()))) { throw new ExpiredCredentialsException("token以失效,請重新登陸!"); } } // 執行自動登陸 return executeLogin(servletRequest, servletResponse); } /** * 執行登入出現異常 * @param token * @param e * @param request * @param response * @return */ @Override protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) { HttpServletResponse httpServletResponse = (HttpServletResponse)response; // 1. 判斷是否因異常登陸失敗 Throwable throwable = e.getCause() == null ? e : e.getCause(); // 2.獲取登陸異常資訊以自定義的Resut響應格式返回json資料 Result result = Result.error(throwable.getMessage()); String json = JSONUtil.toJsonStr(result);// hutool的一個json工具 // 3.列印響應 try{ httpServletResponse.getWriter().print(json); }catch (IOException ioException){ } return false; } }
三、springboot中全域性異常處理
前後端分離,我們需要配置異常處理機制,返回一個友好簡單格式給前端。
處理方式:
- 通過@ControllerAdvice來進行統一異常處理
通過@ExceptionHandler(value=RuntimeException.class)指定要捕獲的Exception的各個型別,這個異常處理是全域性的,所有的類似異常都會捕獲。
/** * 全域性異常處理 */ @Slf4j @RestControllerAdvice public class GlobalExcepitonHandler { // 捕捉shiro的異常 @ResponseStatus(HttpStatus.UNAUTHORIZED) @ExceptionHandler(ShiroException.class) public Result handle401(ShiroException e) { return Result.fail(401, e.getMessage(), null); } /** * 處理Assert的異常 */ @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(value = IllegalArgumentException.class) public Result handler(IllegalArgumentException e) throws IOException { log.error("Assert異常:-------------->{}",e.getMessage()); return Result.fail(e.getMessage()); } /** * @Validated 校驗錯誤異常處理 */ @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(value = MethodArgumentNotValidException.class) public Result handler(MethodArgumentNotValidException e) throws IOException { log.error("執行時異常:-------------->",e); // 擷取所有必要的錯誤資訊;只顯示錯誤原因,不會顯示其他cause BY.... BindingResult bindingResult = e.getBindingResult(); ObjectError objectError = bindingResult.getAllErrors().stream().findFirst().get(); return Result.fail(objectError.getDefaultMessage()); } /* * 執行時異常 */ @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(value = RuntimeException.class) public Result handler(RuntimeException e) throws IOException { log.error("執行時異常:-------------->",e); return Result.fail(e.getMessage()); } }
上面我們捕捉了幾個異常:
- ShiroException:shiro丟擲的異常,比如沒有許可權,使用者登入異常
- IllegalArgumentException:處理Assert的異常
- MethodArgumentNotValidException:處理實體校驗的異常
- RuntimeException:捕捉其他異常
1. springboot中實體校驗
使用springboot框架,就自動整合了Hibernate validatior。
第一步:實體屬性上新增校驗規則
@TableName("m_user") public class User implements Serializable { private static final long serialVersionUID = 1L; @TableId(value = "id", type = IdType.AUTO) private Long id; @NotBlank(message = "暱稱不能為空") private String username; private String avatar; @NotBlank(message = "郵箱不能為空") @Email(message = "郵箱格式不正確") private String email; }
第二步:測試實體校驗
採用@Validated註解,實體中有不符合校驗規則的,會丟擲異常,在異常處理中的MethodArgumentNotValidException中捕獲。
@PostMapping("/save") public Object save(@Validated @RequestBody User user) { return user.toString(); }
四、前後端分離的跨域處理
在後臺進行全域性跨域處理
/** * 解決跨域問題 * project: vue-mt-blog * created by Maotao on 2020/6/30 */ @Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**"). allowedOrigins("*"). allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS"). allowCredentials(true). maxAge(3600). allowedHeaders("*"); } }
全域性跨域處理
/** * 解決跨域問題 * project: vue-mt-blog * created by Maotao on 2020/6/30 */ @Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**"). allowedOrigins("*"). allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS"). allowCredentials(true). maxAge(3600). allowedHeaders("*"); } }