Shiro的Session機制
因爲個人方法是改變了Shiro的默認的Session機制,因此這裏先簡單講一下Shiro的機制,簡單瞭解Shiro是怎麼肯定每次訪問的是哪一個用戶的css
Servlet的Session機制
Shiro在JavaWeb中使用到的就是默認的Servlet的Session機制,大體流程以下:html
1.用戶首次發請求前端
2.服務器接收到請求以後,不管你有沒有權限訪問到資源,在返回響應的時候,服務器都會生成一個Session用來儲存該用戶的信息,而後生成SessionId做爲對應的Keyjava
3.服務器會在響應中,用jsessionId這個名字,把這個SessionId以Cookie的方式發給客戶(就是Set-Cookie響應頭)web
4.因爲已經設置了Cookie,下次訪問的時候,服務器會自動識別到這個SessionId而後找到你上次對應的Sessionspring
Shiro帶來的變化
而結合Shiro以後,上面的第二步和第三步會發生小變化:數據庫
2.—>服務器不但會建立Session,還會建立一個Subject對象(就是Shiro中用來表明當前用戶的類),也用這個SessionId做爲Key綁定apache
3.—>第二次接受到請求的時候,Shiro會從請求頭中找到SessionId,而後去尋找對應的Subject而後綁定到當前上下文,這時候Shiro就能知道來訪的是誰了json
個人思路
主要思想是:用JWT Token來代替Shiro本來返回的Session
api
工做流程:
- 用戶登陸
- 若成功則JWT生成token,返回給前端
- 用戶第二次攜帶JWT來訪問接口
- 服務器解析JWT,認證成功
- 服務器會校驗是否有權限
代碼實現
1、導入JWT相關包
1.一、pom.xml裏寫入
<dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.4.1</version> </dependency>
1.二、 JWT工具類
JwtUtils,代碼以下:
import cn.hutool.core.util.StrUtil; import com.alibaba.fastjson.JSONObject; import com.auth0.jwt.JWT; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.interfaces.DecodedJWT; import com.tg.higo.couponplatform.entity.dto.User; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Calendar; import java.util.Date; import java.util.HashMap; import java.util.Map; /** * JWT憑證驗證工具 * * @author LuoJianXing */ public class JwtUtils { private static final Algorithm ALGORITHM; /** * 登陸Token有效期 */ public static final Integer VALID_SECONDS = 3600 * 10; private static final Logger logger = LoggerFactory.getLogger(JwtUtils.class); static { ALGORITHM = Algorithm.HMAC256("tghigo"); } /** * 編碼登陸用戶 * * @param user * @param validSecond * @return */ public static String encodeToken(User user, int validSecond) { Calendar calendar = Calendar.getInstance(); calendar.add(Calendar.SECOND, validSecond); JSONObject obj = new JSONObject(); obj.put("account",user.getAccount()); obj.put("role",user.getRole()); if (StringUtil.isNotEmpty(user.getOrgId())){ obj.put("orgId", user.getOrgId()); } String token = JWT.create().withClaim("user", obj.toJSONString()) .withExpiresAt(calendar.getTime()) .sign(ALGORITHM); return token; } public static String encodeToken(User user) { return encodeToken(user, VALID_SECONDS); } /** * 解碼登陸用戶 * * @param token * @return */ public static User decodeToken(String token) { DecodedJWT decodedJWT = JWT.decode(token); String userStr = decodedJWT.getClaim("user").as(String.class); return JSONObject.parseObject(userStr, User.class); } /** * 驗證JWT Token * * @param token * @return */ public static boolean validToken(String token) { if (StringUtil.isBlank(token)) { return false; } return JWT.decode(token).getExpiresAt().after(Calendar.getInstance().getTime()); } //編碼api方式登陸用戶 public static String encode(String openid, int validSecond) { Calendar calendar = Calendar.getInstance(); calendar.add(Calendar.SECOND, validSecond); return JWT.create() .withClaim("openid", openid) .withExpiresAt(calendar.getTime()).sign(ALGORITHM); } //解碼api方式登陸用戶 public static String decode(String token) { DecodedJWT decodedJWT = JWT.decode(token); return decodedJWT.getClaim("openid").as(String.class); } }
2、整合shiro
2.一、導入shiro相關包 pom.xml裏寫入
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.5.1</version> </dependency>
2.二、 關閉 自動建立session
package com.tg.higo.couponplatform.shiro; import org.apache.shiro.subject.Subject; import org.apache.shiro.subject.SubjectContext; import org.apache.shiro.web.mgt.DefaultWebSubjectFactory; public class JwtDefaultSubjectFactory extends DefaultWebSubjectFactory { @Override public Subject createSubject(SubjectContext context) { // 不建立 session context.setSessionCreationEnabled(false); return super.createSubject(context); } }
2.三、自定義過濾器
package com.tg.higo.couponplatform.shiro; import com.tg.higo.couponplatform.common.utils.StringUtil; import org.apache.shiro.web.filter.AccessControlFilter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; public class JwtFilter extends AccessControlFilter { /** * 日誌對象 */ protected Logger logger = LoggerFactory.getLogger(AdminShiroRealm.class); /* * 1. 返回true,shiro就直接容許訪問url * 2. 返回false,shiro纔會根據onAccessDenied的方法的返回值決定是否容許訪問url * */ @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception { logger.warn("isAccessAllowed 方法被調用"); //這裏先讓它始終返回false來使用onAccessDenied()方法 return false; } /** * 返回結果爲true代表登陸經過 */ @Override protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception { logger.warn("onAccessDenied 方法被調用"); //這個地方和前端約定,要求前端將jwtToken放在請求的Header部分 //因此之後發起請求的時候就須要在Header中放一個Authorization,值就是對應的Token HttpServletRequest request = (HttpServletRequest) servletRequest; String jwt = getTokenFromRequest(request); if(StringUtil.isBlank(jwt)){ onLoginFail(servletResponse); //調用下面的方法向客戶端返回錯誤信息 return false; } logger.info("請求的 Header 中藏有 jwtToken {}", jwt); JwtToken jwtToken = new JwtToken(jwt); /* * 下面就是固定寫法 * */ try { // 委託 realm 進行登陸認證 //因此這個地方最終仍是調用JwtRealm進行的認證 getSubject(servletRequest, servletResponse).login(jwtToken); //也就是subject.login(token) } catch (Exception e) { // e.printStackTrace(); logger.info(e.getMessage()); onLoginFail(servletResponse); //調用下面的方法向客戶端返回錯誤信息 return false; } return true; //執行方法中沒有拋出異常就表示登陸成功 } // 從請求中獲取 token private String getTokenFromRequest(ServletRequest request) { HttpServletRequest req = (HttpServletRequest) request; return req.getHeader("Token"); } //登陸失敗時默認返回 401 狀態碼 private void onLoginFail(ServletResponse response) throws IOException { HttpServletResponse httpResponse = (HttpServletResponse) response; httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED); httpResponse.getWriter().write("login error"); } }
2.四、JwtToken 類
package com.tg.higo.couponplatform.shiro; import org.apache.shiro.authc.AuthenticationToken; public class JwtToken implements AuthenticationToken { private String token; public JwtToken(String token) { this.token = token; } //yonghu信息 @Override public Object getPrincipal() { return token; } @Override public Object getCredentials() { return token; } }
2.五、 重寫Realm ,定義認證,受權
package com.tg.higo.couponplatform.shiro; import com.tg.higo.couponplatform.common.utils.JwtUtils; import com.tg.higo.couponplatform.entity.dto.User; import org.apache.shiro.authc.*; import org.apache.shiro.authz.AuthorizationException; 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.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.HashSet; import java.util.Set; public class AdminShiroRealm extends AuthorizingRealm { /** * 日誌對象 */ protected Logger logger = LoggerFactory.getLogger(AdminShiroRealm.class); /* * 多重寫一個support * 標識這個Realm是專門用來驗證JwtToken * 不負責驗證其餘的token(UsernamePasswordToken) * */ @Override public boolean supports(AuthenticationToken token) { //這個token就是從過濾器中傳入的jwtToken return token instanceof JwtToken; } //認證 //這個token就是從過濾器中傳入的jwtToken @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { String jwt = (String) token.getCredentials(); if (!JwtUtils.validToken(jwt)) { throw new UnknownAccountException(); } //下面是驗證這個user是不是真實存在的 User user = JwtUtils.decodeToken(jwt);//判斷數據庫中username是否存在 // log.info("在使用token登陸"+username); return new SimpleAuthenticationInfo(jwt,jwt,getName()); //這裏返回的是相似帳號密碼的東西,可是jwtToken都是jwt字符串。還須要一個該Realm的類名 } // 受權 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { if (principalCollection == null) { throw new AuthorizationException("PrincipalCollection method argument cannot be null."); } // User user= JwtUtils.decodeToken(principalCollection.toString()); SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); //從數據庫查詢角色 Set<String> roleSet = new HashSet<>(); roleSet.add("ghfhgfg"); authorizationInfo.setRoles(roleSet); // //從數據庫查詢權限 Set<String> permsSet = new HashSet<>(); permsSet.add("/applet/getZone"); authorizationInfo.setStringPermissions(permsSet); return authorizationInfo; } }
2.六、 後臺自定義過濾器
package com.tg.higo.couponplatform.shiro; import com.tg.higo.couponplatform.common.utils.JwtUtils; import com.tg.higo.couponplatform.entity.dto.User; import org.apache.shiro.subject.Subject; import org.apache.shiro.web.filter.authc.FormAuthenticationFilter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletResponse; import java.io.PrintWriter; /** *@Description 後臺自定義過濾器 **/ public class AdminAuthenticationFilter extends FormAuthenticationFilter { private Logger logger= LoggerFactory.getLogger(AdminAuthenticationFilter.class); public AdminAuthenticationFilter() { super(); } @Override public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { Subject subject = getSubject(request,response); String url = getPathWithinApplication(request); logger.debug("當前用戶正在訪問的 url => " + url); if(subject != null && subject.getPrincipal()!=null ) { logger.debug("當前用戶" + subject.getPrincipal().toString()); // logger.debug("getStartTimestamp=" + subject.getSession().getStartTimestamp()); // logger.debug("getTimeout=" + subject.getSession().getTimeout()); // logger.debug("getLastAccessTime=" + subject.getSession().getLastAccessTime()); logger.debug("isAuthenticated=" + subject.isAuthenticated()); } logger.debug("isPermitted=" + subject.isPermitted(url)); return subject.isPermitted(url); } @Override protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { HttpServletResponse res = (HttpServletResponse)response; res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader("Content-Type","application/json"); res.setStatus(HttpServletResponse.SC_OK); res.setCharacterEncoding("UTF-8"); Subject subject = getSubject(request,response); Object currentUser = JwtUtils.decodeToken((String) subject.getPrincipal()); // ApiResult apiResult; logger.debug("isAuthenticated=" + subject.isAuthenticated()); logger.debug("instanceof=" + (currentUser instanceof User)); logger.debug("isPermitted=" + subject.isPermitted(getPathWithinApplication(request))); PrintWriter writer = res.getWriter(); logger.info("subject.isAuthenticated:",subject.isAuthenticated()); logger.info("currentUser instanceof User:",currentUser instanceof User); if(subject.isAuthenticated() && currentUser instanceof User){ writer.write("沒有權限,請聯繫管理員受權。"); // apiResult=new ApiResult(403,"沒有權限,請聯繫管理員受權。"); }else{ writer.write("登陸超時,請從新登陸。"); // apiResult=new ApiResult(401,"登陸超時,請從新登陸。"); } // writer.write(JSON.toJSONString(apiResult)); writer.close(); return false; } }
2.七、 Shiro相關配置
package com.tg.higo.couponplatform.config; import com.tg.higo.couponplatform.shiro.*; import org.apache.shiro.authz.permission.PermissionResolver; import org.apache.shiro.cache.CacheManager; import org.apache.shiro.cache.MemoryConstrainedCacheManager; import org.apache.shiro.mgt.DefaultSessionStorageEvaluator; import org.apache.shiro.mgt.DefaultSubjectDAO; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.realm.Realm; import org.apache.shiro.spring.LifecycleBeanPostProcessor; import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.servlet.Filter; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; /** * @Description Shiro相關配置 **/ @Configuration public class ShiroConfig { @Bean public Realm adminShiroRealm() { AdminShiroRealm adminShiroRealm = new AdminShiroRealm(); //將自定義的權限匹配器注入到自定義 Realm 中 // adminShiroRealm.setPermissionResolver(urlPermissionResolver()); // //設置解密規則 // adminShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher()); return adminShiroRealm; } // @Bean // public PermissionResolver urlPermissionResolver(){ // return new UrlPermissionResolver(); // } @Bean public CacheManager cacheManager() { return new MemoryConstrainedCacheManager(); } @Bean public SecurityManager securityManager() { DefaultWebSecurityManager sm = new DefaultWebSecurityManager(); // sm.setAuthenticator(modularRealmAuthenticator()); List<Realm> realms = new ArrayList<>(); realms.add(adminShiroRealm()); // realms.add(userShiroRealm()); // realms.add(freeRealm()); sm.setCacheManager(cacheManager()); //注入自定義sessionManager // sm.setSessionManager(sessionManager()); sm.setRealms(realms); // 關閉 ShiroDAO 功能 DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO(); DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator(); // 不須要將 Shiro Session 中的東西存到任何地方(包括 Http Session 中) defaultSessionStorageEvaluator.setSessionStorageEnabled(false); subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator); sm.setSubjectDAO(subjectDAO); //禁止Subject的getSession方法 sm.setSubjectFactory(subjectFactory()); return sm; } @Bean public JwtDefaultSubjectFactory subjectFactory() { return new JwtDefaultSubjectFactory(); } public AdminAuthenticationFilter adminAuthenticationFilter(){ return new AdminAuthenticationFilter(); } public JwtFilter jwtFilter() { return new JwtFilter(); } @Bean(name = "shiroFilter") public ShiroFilterFactoryBean getShiroFilterFactoryBean(SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean(); shiroFilter.setSecurityManager(securityManager); Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>(); //配置不會被攔截的連接,順序判斷 filterChainDefinitionMap.put("/", "anon"); filterChainDefinitionMap.put("/*.ico", "anon"); filterChainDefinitionMap.put("/static/js/**", "anon"); filterChainDefinitionMap.put("/static/css/**", "anon"); filterChainDefinitionMap.put("/static/fonts/**", "anon"); filterChainDefinitionMap.put("/static/**", "anon"); //接口文檔 filterChainDefinitionMap.put("/doc.html", "anon"); filterChainDefinitionMap.put("/webjars/**", "anon"); filterChainDefinitionMap.put("/swagger-resources", "anon"); filterChainDefinitionMap.put("/api-docs", "anon"); filterChainDefinitionMap.put("/v2/**", "anon"); filterChainDefinitionMap.put("/swagger.json", "anon"); //後臺登陸 filterChainDefinitionMap.put("/admin/login", "anon"); // 批量同步密碼接口 filterChainDefinitionMap.put("/api/test/updateMobile", "anon"); //退出登陸 filterChainDefinitionMap.put("/**/logout", "anon"); //authc:全部url必須經過認證才能訪問,anon:全部url均可以匿名訪問 filterChainDefinitionMap.put("/**", "jwt,adminFilter"); shiroFilter.setFilterChainDefinitionMap(filterChainDefinitionMap); //自定義過濾器 Map<String, Filter> filterMap = new LinkedHashMap<>(); filterMap.put("jwt", jwtFilter()); filterMap.put("adminFilter", adminAuthenticationFilter()); shiroFilter.setFilters(filterMap); return shiroFilter; } /** * Shiro生命週期處理器 * @return */ @Bean public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() { return new LifecycleBeanPostProcessor(); } @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) { AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; } }
完成
3、登陸接口
/** * 帳號登陸 * * @param account:數字和下劃線 * @param password:2次md5加密 * @return */ @RequestMapping("/login") @ResponseBody public JSONObject login(String account, String password) { return accountInfoService.login(account, password); } //帳號登陸 public JSONObject login(String account, String password) { //判斷參數 if (StringUtil.isBlank(account) || StringUtil.isBlank(password)) { return ReturnEntity.missParams(); } //獲取帳號 AccountInfoDto accountInfoDto = AccountInfoDao.selectByAccount(account); if (accountInfoDto == null) { return ReturnEntity.fail("帳號有誤"); } if (!accountInfoDto.getPassword().equals(password)) { return ReturnEntity.fail("密碼錯誤"); } if (accountInfoDto.getState() == User.closeState) { return ReturnEntity.fail("帳號已經禁用"); } //獲取返回token User user = new User(); user.setAccount(accountInfoDto.getAccount()); user.setRole(accountInfoDto.getRole()); if (user.getRole() == User.orgRole) { user.setOrgId(accountInfoDto.getOrgId()); } else { user.setOrgId("0"); } String token = JwtUtils.encodeToken(user); //設置好token,後來會在全局處理的時候放入響應裏 req.setAttribute("token", token); JSONObject data = new JSONObject(); data.put("token", token); // data.put("expire", JwtUtils.VALID_SECONDS); // data.put("role", user.getRole()); return ReturnEntity.success(data, "登陸成功"); }
完成
測試:
後續請求帶token
就能夠了