您好,登錄后才能下訂單哦!
這篇文章給大家介紹怎么在Spring boot中使用shiro和jwt實現前后端分離,內容非常詳細,感興趣的小伙伴們可以參考借鑒,希望對大家能有所幫助。
<!-- shiro包 --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.4.0</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-ehcache</artifactId> <version>1.4.0</version> </dependency> <!--JWT依賴--> <!--JWT--> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.4.0</version> </dependency> <!--JJWT--> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency>
創建shiro的自定義的Realm
代碼如下:
package com.serverprovider.config.shiro.userRealm; import com.spring.common.auto.autoUser.AutoUserModel; import com.spring.common.auto.autoUser.extend.AutoModelExtend; import com.serverprovider.config.shiro.jwt.JWTCredentialsMatcher; import com.serverprovider.config.shiro.jwt.JwtToken; import com.serverprovider.service.loginService.LoginServiceImpl; import com.util.Redis.RedisUtil; import com.util.ReturnUtil.SecretKey; import com.util.encryption.JWTDecodeUtil; import io.jsonwebtoken.Claims; import org.apache.log4j.Logger; import org.apache.shiro.authc.*; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.ExceptionHandler; import java.util.HashSet; import java.util.List; import java.util.Set; public class UserRealm extends AuthorizingRealm { private Logger logger = Logger.getLogger(UserRealm.class); @Autowired private LoginServiceImpl loginService; public UserRealm(){ //這里使用我們自定義的Matcher驗證接口 this.setCredentialsMatcher(new JWTCredentialsMatcher()); } /** * 必須重寫此方法,不然Shiro會報錯 */ @Override public boolean supports(AuthenticationToken token) { return token instanceof JwtToken; } /** * shiro 身份驗證 * @param token * @return boolean * @throws AuthenticationException 拋出的異常將有統一的異常處理返回給前端 * */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)throws AuthenticationException { /** * AuthenticationToken * JwtToken重寫了AuthenticationToken接口 并創建了一個接口token的變量 * 因為在filter我們將token存入了JwtToken的token變量中 * 所以這里直接getToken()就可以獲取前端傳遞的token值 */ String JWTtoken = ((JwtToken) token).getToken(); /** * Claims對象它最終是一個JSON格式的對象,任何值都可以添加到其中 * token解密 轉換成Claims對象 */ Claims claims = JWTDecodeUtil.parseJWT(JWTtoken, SecretKey.JWTKey); /** * 根據JwtUtil加密方法加入的參數獲取數據 * 查詢數據庫獲得對象 * 如為空:拋出異常 * 如驗證失敗拋出 AuthorizationException */ String username = claims.getSubject(); String password = (String) claims.get("password"); AutoModelExtend principal = loginService.selectLoginModel(username,password); return new SimpleAuthenticationInfo(principal, JWTtoken,"userRealm"); } @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { SimpleAuthorizationInfo info = null; /** * PrincipalCollection對象 * 文檔里面描述:返回從指定的Realm 僅作為Collection 返回的單個Subject的對象,如果沒有來自該領域的任何對象,則返回空的Collection。 * 在登錄接口放入權限注解返回的錯誤信息:Subject.login(AuthenticationToken)或SecurityManager啟用'Remember Me'功能后成功自動獲取這些標識主體 * 當調用Subject.login()方法成功后 PrincipalCollection會自動獲得該對象 如沒有認證過或認證失敗則返回空的Collection并拋出異常 * getPrimaryPrincipal():返回在應用程序范圍內使用的主要對象,以唯一標識擁有帳戶。 */ Object principal = principals.getPrimaryPrincipal(); /** * 得到身份對象 * 查詢該用戶的權限信息 */ AutoUserModel user = (AutoUserModel) principal; List<String> roleModels = loginService.selectRoleDetails(user.getId()); try { /** * 創建一個Set,來放置用戶擁有的權限 * 創建 SimpleAuthorizationInfo, 并將辦好權限列表的Set放入. */ Set<String> rolesSet = new HashSet(); for (String role : roleModels) { rolesSet.add(role); } info = new SimpleAuthorizationInfo(); info.setStringPermissions(rolesSet); // 放入權限信息 }catch (Exception e){ throw new AuthenticationException("授權失敗!"); } return info; } }
這個授權方法遇到的坑比較少,就是在最終驗證的時候網上很照抄過來的帖子一點都沒有驗證就粘貼賦值,在這里嚴重吐槽。
在使用jwt最為token而取消shiro傳統的session時候,我們的需要重寫shiro的驗證接口 CredentialsMatcher,在 自定義的realm
中我們加入我們重寫的驗證方法,在調用SimpleAuthenticationInfo()方法進行驗證的時候,shiro就會使用重寫的驗證接口。
此處為大坑。
貼上代碼如下:
import com.spring.common.auto.autoUser.extend.AutoModelExtend; import org.apache.log4j.Logger; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.credential.CredentialsMatcher; /** * CredentialsMatcher * 接口由類實現,該類可以確定是否提供了AuthenticationToken憑證與系統中存儲的相應帳戶的憑證相匹配。 * Shiro 加密匹配 重寫匹配方法CredentialsMatcher 使用JWTUtil 匹配方式 */ public class JWTCredentialsMatcher implements CredentialsMatcher { private Logger logger = Logger.getLogger(JWTCredentialsMatcher.class); /** * Matcher中直接調用工具包中的verify方法即可 */ @Override public boolean doCredentialsMatch(AuthenticationToken authenticationToken, AuthenticationInfo authenticationInfo) { String token = (String) ((JwtToken)authenticationToken).getToken(); AutoModelExtend user = (AutoModelExtend)authenticationInfo.getPrincipals().getPrimaryPrincipal(); //得到DefaultJwtParser Boolean verify = JwtUtil.isVerify(token, user); logger.info("JWT密碼效驗結果="+verify); return verify; } }
shiro的配置項 ShiroConfiguration代碼如下:
import com.serverprovider.config.shiro.shiroSysFile.JwtFilter; import com.serverprovider.config.shiro.userRealm.UserRealm; import org.apache.log4j.Logger; import org.apache.shiro.mgt.DefaultSessionStorageEvaluator; import org.apache.shiro.mgt.DefaultSubjectDAO; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.mgt.SessionStorageEvaluator; import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.apache.shiro.web.mgt.DefaultWebSessionStorageEvaluator; import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.servlet.Filter; import java.util.*; @Configuration public class ShiroConfiguration { private Logger logger = Logger.getLogger(ShiroConfiguration.class); @Bean("shiroFilter") public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); //攔截器 Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>(); // 配置不會被攔截的鏈接 順序判斷 filterChainDefinitionMap.put("/login/**", "anon"); // 添加自己的過濾器并且取名為jwt Map<String, Filter> filterMap = new HashMap<String, Filter>(); filterMap.put("jwt", new JwtFilter()); shiroFilterFactoryBean.setFilters(filterMap); //<!-- 過濾鏈定義,從上向下順序執行,一般將/**放在最為下邊 filterChainDefinitionMap.put("/**", "jwt"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean; } /** * 禁用session, 不保存用戶登錄狀態。保證每次請求都重新認證。 * 需要注意的是,如果用戶代碼里調用Subject.getSession()還是可以用session */ @Bean protected SessionStorageEvaluator sessionStorageEvaluator(){ DefaultWebSessionStorageEvaluator sessionStorageEvaluator = new DefaultWebSessionStorageEvaluator(); sessionStorageEvaluator.setSessionStorageEnabled(false); return sessionStorageEvaluator; } @Bean("securityManager") public SecurityManager securityManager(UserRealm userRealm) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(userRealm); /* * 關閉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); securityManager.setSubjectDAO(subjectDAO); return securityManager; } /** * 創建自定義的UserRealm @bean */ @Bean("userRealm") public UserRealm shiroRealm() { UserRealm shiroRealm = new UserRealm(); return shiroRealm; } //自動創建代理,沒有這個鑒權可能會出錯 @Bean public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() { DefaultAdvisorAutoProxyCreator autoProxyCreator = new DefaultAdvisorAutoProxyCreator(); autoProxyCreator.setProxyTargetClass(true); return autoProxyCreator; } /** * 開啟shiro aop注解支持. * 使用代理方式;所以需要開啟代碼支持; * * @param * @return */ @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) { AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; }
到這里shiro配置就全部完成了
下面開始配置jwt:
首先我們需要重寫 AuthenticationToken接口 此接口的作用
負責把shiro中username,password生成用于驗證的token的封裝類
所有我們需要去實現這個接口,封裝我們自己生成的JWT生成的token
import org.apache.shiro.authc.AuthenticationToken; /** * AuthenticationToken: shiro中負責把username,password生成用于驗證的token的封裝類 * 我們需要自定義一個對象用來包裝token。 */ public class JwtToken implements AuthenticationToken { private String token; public JwtToken(String token) { this.token = token; } public String getToken() { return token; } public void setToken(String token) { this.token = token; } @Override public Object getPrincipal() { return null; } @Override public Object getCredentials() { return null; } }
因為我們是前后端分離項目所有我們不存在session驗證之類的 所有每次請求都需要攜帶token,所以每次請求我們都需要去驗證token的真實性,所有我們需要去實現 BasicHttpAuthenticationFilter過濾器
BasicHttpAuthenticationFilter繼承 AuthenticatingFilter 過濾器其能夠自動地進行基于所述傳入請求的認證嘗試。此實現是每個基本HTTP身份驗證規范的Java實現 , 通過此過濾器得到HTTP請求資源獲取Authorization傳遞過來的token參數 獲取subject對象進行身份驗證
代碼如下:
import com.alibaba.fastjson.JSONObject; import com.serverprovider.config.shiro.jwt.JwtToken; import com.util.Util.utilTime; import org.apache.log4j.Logger; import org.apache.shiro.subject.Subject; import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RequestMethod; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; import java.util.concurrent.TimeUnit; /** * * BasicHttpAuthenticationFilter繼承 AuthenticatingFilter 過濾器 * 其能夠自動地進行基于所述傳入請求的認證嘗試。 * BasicHttpAuthenticationFilter 基本訪問認證過濾器 * 此實現是每個基本HTTP身份驗證規范的Java實現 * 通過此過濾器得到HTTP請求資源獲取Authorization傳遞過來的token參數 * 獲取subject對象進行身份驗證 * * */ public class JwtFilter extends BasicHttpAuthenticationFilter { Logger logger = Logger.getLogger(JwtFilter.class); /** * 應用的HTTP方法列表配置基本身份驗證篩選器。 * 獲取 request 請求 拒絕攔截登錄請求 * 執行登錄認證方法 * * @param request * @param response * @param mappedValue * @return */ @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { HttpServletRequest httpServletRequest = (HttpServletRequest) request; String requestURI = httpServletRequest.getRequestURI(); if (requestURI.equals("/user/login/verifyUser") || requestURI.equals("/user/register")) { return true; } else { try { executeLogin(request, response); return true; } catch (Exception e) { e.printStackTrace(); return false; } } } /** * Authorization攜帶的參數為token * JwtToken實現了AuthenticationToken接口封裝了token參數 * 通過getSubject方法獲取 subject對象 * login()發送身份驗證 * * 為什么需要在Filter中調用login,不能在controller中調用login? * 由于Shiro默認的驗證方式是基于session的,在基于token驗證的方式中,不能依賴session做為登錄的判斷依據. * @param request * @param response */ @Override protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception { HttpServletResponse httpServletResponse = (HttpServletResponse) response; try{ HttpServletRequest httpServletRequest = (HttpServletRequest) request; String token = httpServletRequest.getHeader("Authorization"); JwtToken jwtToken = new JwtToken(token); // 提交給realm進行登入,如果錯誤他會拋出異常并被捕獲 Subject subject = getSubject(request, response); subject.login(jwtToken); logger.info("JWT驗證用戶信息成功"); // 如果沒有拋出異常則代表登入成功,返回true return true; }catch (Exception e){ /* * * 這個問題糾結了好久 * 原生的shiro驗證失敗會進入全局異常 但是 和JWT結合以后卻不進入了 之前一直想不通 * 原因是 JWT直接在過濾器里驗證 驗證成功與否 都是直接返回到過濾器中 成功在進入controller * 失敗直接返回進入springboot自定義異常處理頁面 */ JSONObject responseJSONObject = new JSONObject(); responseJSONObject.put("result","401"); responseJSONObject.put("resultCode","token無效,請重新獲取。"); responseJSONObject.put("resultData","null"); responseJSONObject.put("resultTime", utilTime.StringDate()); PrintWriter out = null; httpServletResponse.setCharacterEncoding("UTF-8"); httpServletResponse.setContentType("application/json; charset=utf-8"); logger.info("返回是"); logger.info(responseJSONObject.toString()); out = httpServletResponse.getWriter(); out.append(responseJSONObject.toString()); } return false; } /** * 對跨域提供支持 */ @Override protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception { HttpServletRequest httpServletRequest = (HttpServletRequest) request; HttpServletResponse httpServletResponse = (HttpServletResponse) response; httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin")); httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE"); httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers")); // 跨域時會首先發送一個option請求,這里我們給option請求直接返回正常狀態 if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) { httpServletResponse.setStatus(HttpStatus.OK.value()); return false; } return super.preHandle(request, response); } }
貼上 加密解密校驗的工具類
import com.spring.common.auto.autoUser.extend.AutoModelExtend; import com.util.ReturnUtil.SecretKey; import io.jsonwebtoken.*; import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.UUID;; /* @author hw * @create 2019-04-16 10.12 * @desc JWT工具類 **/ public class JwtUtil { /** * 用戶登錄成功后生成Jwt * 使用Hs256算法 私匙使用用戶密碼 * * @param ttlMillis jwt過期時間 * @param user 登錄成功的user對象 * @return */ public static String createJWT(long ttlMillis, AutoModelExtend user) { //指定簽名的時候使用的簽名算法,也就是header那部分,jjwt已經將這部分內容封裝好了。 SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; //生成JWT的時間 long nowMillis = System.currentTimeMillis(); Date now = new Date(nowMillis); //創建payload的私有聲明(根據特定的業務需要添加,如果要拿這個做驗證,一般是需要和jwt的接收方提前溝通好驗證方式的) Map<String, Object> map = new HashMap<String, Object>(); map.put("id", user.getId()); map.put("username", user.getAuto_username()); map.put("password", user.getAuto_password()); //生成簽名的時候使用的秘鑰secret,這個方法本地封裝了的,一般可以從本地配置文件中讀取, // 切記這個秘鑰不能外露哦。它就是你服務端的私鑰,在任何場景都不應該流露出去。一旦客戶端得知這個secret, 那就意味著客戶端是可以自我簽發jwt了。 String key = SecretKey.JWTKey; //生成簽發人 String subject = user.getAuto_username(); //下面就是在為payload添加各種標準聲明和私有聲明了 //這里其實就是new一個JwtBuilder,設置jwt的body JwtBuilder builder = Jwts.builder() //如果有私有聲明,一定要先設置這個自己創建的私有的聲明,這個是給builder的claim賦值,一旦寫在標準的聲明賦值之后,就是覆蓋了那些標準的聲明的 .setClaims(map) //設置jti(JWT ID):是JWT的唯一標識,根據業務需要,這個可以設置為一個不重復的值,主要用來作為一次性token,從而回避重放攻擊。 .setId(UUID.randomUUID().toString()) //iat: jwt的簽發時間 .setIssuedAt(now) //代表這個JWT的主體,即它的所有人,這個是一個json格式的字符串,可以存放什么userid,roldid之類的,作為什么用戶的唯一標志。 .setSubject(subject) //設置簽名使用的簽名算法和簽名使用的秘鑰 .signWith(signatureAlgorithm, key); if (ttlMillis >= 0) { long expMillis = nowMillis + ttlMillis; Date exp = new Date(expMillis); //設置過期時間 builder.setExpiration(exp); } return builder.compact(); } /** * 校驗token * 在這里可以使用官方的校驗,我這里校驗的是token中攜帶的密碼于數據庫一致的話就校驗通過 * * @param token * @return */ public static Boolean isVerify(String token, AutoModelExtend userModelExtend) { try { //得到DefaultJwtParser Claims claims = Jwts.parser() //設置簽名的秘鑰 .setSigningKey(SecretKey.JWTKey) //設置需要解析的jwt .parseClaimsJws(token).getBody(); if (claims.get("password").equals(userModelExtend.getAuto_password())) { return true; } } catch (Exception exception) { return false; } return null; } /** * Token的解密 * @param token 加密后的token * @param secret 簽名秘鑰,和生成的簽名的秘鑰一模一樣 * @return */ public static Claims parseJWT(String token, String secret) { //得到DefaultJwtParser Claims claims = Jwts.parser() //設置簽名的秘鑰 .setSigningKey(secret) //設置需要解析的jwt .parseClaimsJws(token).getBody(); return claims; } }
關于怎么在Spring boot中使用shiro和jwt實現前后端分離就分享到這里了,希望以上內容可以對大家有一定的幫助,可以學到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。