應用場景:html
因爲項目後端管理系統不僅是一個服務,並且不僅在Web端運行,還會在移動端App使用,想要使用JWT方式進行無狀態的RESTful API交互,也就是登陸後生成token並返回給前端,前端每次請求時都在請求頭裏面添加token,後端驗證有效性。因此Shiro自帶的Session要禁用掉,同時要從新寫JWT的過濾器。前端
使用shiro+ssm+jwt的一些前期準備:java
禁用session
JWT實現工具,這個網上能夠找到不少,這裏就不貼代碼了
自定義realm,繼承AuthorizingRealm,重寫認證和受權兩個方法
自定義filter,繼承AccessControlFilter,重寫方法isAccessAllowed,onAccessDeniedweb
1、首先,禁用shiro的session,先添加shiro的依賴到pomspring
<!--Shiro--> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.4.0</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-web</artifactId> <version>1.4.0</version> </dependency> <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>
2、禁用 session數據庫
package com.tg.higo.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); } }
配置
applicationContext-shiro.xml
<bean id="subjectFactory" class="com.tg.higo.shiro.JwtDefaultSubjectFactory"></bean> <!-- 安全管理器 --> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <!-- 配置 realm --> <property name="realm" ref="bosRealm"/> <property name="subjectFactory" ref="subjectFactory"/> </bean>
3、 重寫filter 過濾器apache
package com.tg.higo.shiro; import com.tg.higo.common.utils.StringUtil; import org.apache.shiro.web.filter.AccessControlFilter; import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter; 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; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; 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; } // String jwt = request.getHeader("Authorization"); 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"); } }
4、 重寫Realm ,定義認證,受權後端
package com.tg.higo.shiro; import com.tg.higo.common.utils.JwtUtils; import com.tg.higo.dao.UserDtoMapper; import com.tg.higo.exception.RRException; import com.tg.higo.model.UserDto; import com.tg.higo.model.dto.User; import org.apache.commons.lang.StringUtils; 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.apache.shiro.util.ByteSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Resource; 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 (jwt == null) { // // throw new NullPointerException("jwtToken 不容許爲空"); // } //判斷 // JwtUtils jwtUtil = new JwtUtils(); if (!JwtUtils.validToken(jwt)) { throw new UnknownAccountException(); } //下面是驗證這個user是不是真實存在的 String username = (String) JwtUtils.decodeToken(jwt).getAccount();//判斷數據庫中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(); // UserDto user = (UserDto) principalCollection.getPrimaryPrincipal(); //從數據庫查詢角色 Set<String> roleSet = new HashSet<>(); roleSet.add("ghfhgfg"); authorizationInfo.setRoles(roleSet); // //從數據庫查詢權限 Set<String> permsSet = new HashSet<>(); permsSet.add("/account/update/state"); authorizationInfo.setStringPermissions(permsSet); return authorizationInfo; } }
5、 shiro配置文件,applicationContext-shiro.xml緩存
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:util="http://www.springframework.org/schema/util" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-2.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd "> <!-- 配置 shiro 核心過濾器:shiroFilter --> <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <!-- 安全管理器 --> <property name="securityManager" ref="securityManager"/> <!--<!– 未登錄跳轉的頁面 –>--> <!--<property name="loginUrl" value="/login.jsp"/> --> <!--<!– 受權失敗跳轉的頁面 –>--> <!--<property name="unauthorizedUrl" value="/noauth.jsp"/> --> <property name="filters"> <util:map> <entry key="jwt" value-ref="jwtFilter" /> <!--<entry key="adminFilter" value-ref="adminFilter" />--> </util:map> </property> <!-- 配置 url 過濾規則 --> <property name="filterChainDefinitions"> <value> <!-- 所有資源都須要認證才能訪問 --> /account/login=anon <!-- 用戶受權 --> <!--/account/**=adminFilter--> /timing/zeroTask=anon /**=jwt,roles[ghfhgfg1] <!--/**= roles[ghfhgfg]--> </value> </property> </bean> <!-- 安全管理器 --> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <!-- 配置 realm --> <property name="realm" ref="bosRealm"/> <property name="subjectFactory" ref="subjectFactory"/> <property name="subjectDAO" ref="subjectDAO"/> <!--<property name="sessionManager" ref="sessionManager" />--> </bean> <!--<!– 一、安全管理器 –>--> <!--<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">--> <!--<property name="realm" ref="shiroDbRealm"></property>--> <!--<!– 設置緩存管理器爲 ehcache –>--> <!--<property name="cacheManager" ref="shiroEhcacheManager"></property>--> <!--<!– 配置sessionManager,提供session管理 –>--> <!--<property name="sessionManager" ref="sessionManager"></property>--> <!--</bean>--> <!-- 自定義 realm --> <bean id="bosRealm" class="com.tg.higo.shiro.AdminShiroRealm"></bean> <!--<bean id="sessionManager" class="com.tg.higo.shiro.CustomSessionManager"> </bean>--> <bean id="subjectFactory" class="com.tg.higo.shiro.JwtDefaultSubjectFactory"></bean> <bean id ="defaultSessionStorageEvaluator" class="org.apache.shiro.mgt.DefaultSessionStorageEvaluator"> <property name="sessionStorageEnabled" value="false"></property> </bean> <bean id ="subjectDAO" class="org.apache.shiro.mgt.DefaultSubjectDAO"> <property name="sessionStorageEvaluator" ref="defaultSessionStorageEvaluator"/> </bean> <!-- 自定義攔截器 --> <bean id="jwtFilter" class="com.tg.higo.shiro.JwtFilter" /> <!--<bean id="adminFilter" class="com.tg.higo.shiro.AdminAuthenticationFilter" />--> <!--<bean class="org.apache.shiro.spring.LifecycleBeanPostProcessor" id="lifecycleBeanPostProcessor" />--> </beans>
6、總結安全
登陸不在走認證,之後每次要受權的接口,先走認證校驗是否有效,再去受權