shiro+jwt+springboot
- 說在前面
- 簡介
- 專案環境(pom.xml)
- 專案結構(各種包和類)
- 鑒權流程
- 具體代碼
- 配置Shiro
- 配置JWTUtils
- 定義JwtFilter
- 定義JwtToken
- 定義兩個Realm
- 兩個"工具人"
- **JwtCredentialsMatcher.java**
- **MultiRealmAuthenticator.java**
- 全域例外處理
- 兩個controller
- LoginController.java
- ArticleController.java
- 介面測驗
說在前面
簡介
最近粗略地了解了一下shiro框架是怎么使用的,但是視頻講解的是前后端不分離的,雖然思路差不多,但還是好難受,而且沒有講解shiro怎么整合jwt,于是我學習了兩天(四處看博客),終于找到一個良心教程,然后大概把demo給整出來了,就是springboot中整合shiro+jwt,實作token登錄然后shiro保證安全什么的,希望能給過渡期的朋友們一點幫助(找不到好的文章是真的很難受的!!!!)具體的一些解釋大家可以康康代碼里面的注釋噢!還有別導錯包了!
專案環境(pom.xml)
jdk 14 + springboot + shiro1.5.3 + jwt3.3.0 +一些依賴
為了方便大家上手,這里沒有用資料庫,不然又一堆環境什么的問題跑不起來
```java
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com</groupId>
<artifactId>szu</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>szu</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--熱部署插件和lombok插件-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--引入jwt-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.3.0</version>
</dependency>
<!--shiro-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-starter</artifactId>
<version>1.5.3</version>
</dependency>
<!--commons-lang-->
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
專案結構(各種包和類)
相關的類和包包我用藍色框框起來的,大家注意別懵了

鑒權流程
我大致畫了一個流程幫助大家理解登錄流程

具體代碼
配置Shiro
1、我們這個ShiroConfig禁用了session
2、添加自定義的jwtFilter過濾器,用來攔截自定義的JWT token
3、使用自定義的MultiRealmAuthenticatoe多Realm認證器,解決認證例外無法正常回傳的問題(這我不太懂,抱歉)
4、JwtRealm+ShiroRealm的雙realm,JwtRealm專門處理 JWT token驗證身份的請求
shiroConfig主要是配置shiro的一些基本策略,讓shiro能被用起來,例如我們最基本的需要 shiroFilter、SecurityManager、自定義realm
`package com.szu.shiro.config;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.Filter;
import com.szu.db.ShiroRealm;
import com.szu.filter.JwtFilter;
import com.szu.shiro.realm.JwtRealm;
import com.szu.shiro.realm.MultiRealmAuthenticator;
import com.szu.utils.JwtCredentialsMatcher;
import org.apache.shiro.authc.credential.CredentialsMatcher;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.authc.pam.AuthenticationStrategy;
import org.apache.shiro.authc.pam.FirstSuccessfulStrategy;
import org.apache.shiro.authc.pam.ModularRealmAuthenticator;
import org.apache.shiro.authz.Authorizer;
import org.apache.shiro.authz.ModularRealmAuthorizer;
import org.apache.shiro.mgt.*;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.session.mgt.SessionManager;
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.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ShiroConfig {
/**
* 交由 Spring 來自動地管理 Shiro-Bean 的生命周期
*/
@Bean
public static LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
/**
* 為 Spring-Bean 開啟對 Shiro 注解的支持
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
@Bean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator app = new DefaultAdvisorAutoProxyCreator();
app.setProxyTargetClass(true);
return app;
}
/**
* 不向 Spring容器中注冊 JwtFilter Bean,防止 Spring 將 JwtFilter 注冊為全域過濾器
* 全域過濾器會對所有請求進行攔截,而本例中只需要攔截除 /login 和 /logout 外的請求
* 另一種簡單做法是:直接去掉 jwtFilter()上的 @Bean 注解
*/
@Bean
public FilterRegistrationBean<Filter> registration(JwtFilter filter) {
FilterRegistrationBean<Filter> registration = new FilterRegistrationBean<Filter>(filter);
registration.setEnabled(false);
return registration;
}
@Bean
public JwtFilter jwtFilter() {
//過濾器如果加了@Compoent就沒必要用這個方法了
return new JwtFilter();
}
/**
* 配置訪問資源需要的權限
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//給工廠bean設定web安全管理器
shiroFilterFactoryBean.setSecurityManager(securityManager);
shiroFilterFactoryBean.setLoginUrl("/login");
shiroFilterFactoryBean.setSuccessUrl("/authorized");
shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized");
// 添加 jwt 專用過濾器,攔截除 /login 和 /logout 外的請求
Map<String, Filter> filterMap = new LinkedHashMap<>();
filterMap.put("jwtFilter", jwtFilter());
shiroFilterFactoryBean.setFilters(filterMap);
//配置系統受限資源以及公共資源
LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
filterChainDefinitionMap.put("/login", "anon"); // 可匿名訪問
filterChainDefinitionMap.put("/logout", "logout"); // 退出登錄
filterChainDefinitionMap.put("/**", "jwtFilter,authc"); // 需登錄才能訪問
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
/**
* 配置 ModularRealmAuthenticator
*/
@Bean
public ModularRealmAuthenticator authenticator() {
ModularRealmAuthenticator authenticator = new MultiRealmAuthenticator();
// 設定多 Realm的認證策略,默認 AtLeastOneSuccessfulStrategy
AuthenticationStrategy strategy = new FirstSuccessfulStrategy();
authenticator.setAuthenticationStrategy(strategy);
return authenticator;
}
/**
* 禁用session, 不保存用戶登錄狀態,保證每次請求都重新認證
*/
@Bean
protected SessionStorageEvaluator sessionStorageEvaluator() {
DefaultSessionStorageEvaluator sessionStorageEvaluator = new DefaultSessionStorageEvaluator();
sessionStorageEvaluator.setSessionStorageEnabled(false);
return sessionStorageEvaluator;
}
/**
* JwtRealm 配置,需實作 Realm 介面
*/
@Bean
JwtRealm jwtRealm() {
JwtRealm jwtRealm = new JwtRealm();
// 設定加密演算法
CredentialsMatcher credentialsMatcher = new JwtCredentialsMatcher();
// 設定加密次數
jwtRealm.setCredentialsMatcher(credentialsMatcher);
return jwtRealm;
}
/**
* ShiroRealm 配置,需實作 Realm 介面
*/
@Bean
ShiroRealm shiroRealm() { //這里其實是模擬的資料庫的類,但是也繼承了AuthorizingRealm
ShiroRealm shiroRealm = new ShiroRealm();
// 設定加密演算法
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher("SHA-1");
// 設定加密次數
credentialsMatcher.setHashIterations(16);
shiroRealm.setCredentialsMatcher(credentialsMatcher);
return shiroRealm;
}
/**
* 配置 DefaultWebSecurityManager
*/
@Bean
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 1.Authenticator
securityManager.setAuthenticator(authenticator());
// 2.Realm
List<Realm> realms = new ArrayList<Realm>(16);
realms.add(jwtRealm());
realms.add(shiroRealm());
securityManager.setRealms(realms); // 配置多個realm
// 3.關閉shiro自帶的session
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
subjectDAO.setSessionStorageEvaluator(sessionStorageEvaluator());
securityManager.setSubjectDAO(subjectDAO);
return securityManager;
}
@Bean
public Authorizer authorizer(){
//這里是個坑,如果沒有這個bean,啟動會報錯,所以得加上
return new ModularRealmAuthorizer();
}
}
配置JWTUtils
jwtutils作用主要是:
定義token過期時間,創建token的密鑰(自定義),自定義存放token的請求頭的名稱
①簽發token
②判斷過期時間
③重繪token
package com.szu.utils;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTCreationException;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.apache.shiro.crypto.SecureRandomNumberGenerator;
import org.springframework.stereotype.Component;
import java.io.UnsupportedEncodingException;
import java.util.Calendar;
import java.util.Date;
import java.util.Map;
@Component
public class JwtUtils {
// 過期時間5分鐘
private static final long EXPIRE_TIME = 5*60*1000;
//自己定制密鑰
public static final String SECRET = "SECRET_VALUE";
//請求頭
public static final String AUTH_HEADER = "X-Authorization-With";
/**
* 驗證token是否正確
* @param token
* @param username
* @param secret
* @return
*/
public static boolean verify(String token, String username, String secret){
try{
Algorithm algorithm = Algorithm.HMAC256(secret);
JWTVerifier verifier = JWT.require(algorithm).withClaim("username",username).build();
verifier.verify(token);
return true;
} catch (JWTVerificationException exception){
return false;
} catch (Exception e){
e.printStackTrace();
return false;
}
}
/**
* 獲得token中的自定義資訊,一般是獲取token的username,無需secret解密也能獲得
* @param token
* @param filed
* @return
*/
public static String getClaimFiled(String token, String filed){
try{
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim(filed).asString();
} catch (JWTDecodeException e){
e.printStackTrace();
return null;
}
}
/**
* 生成簽名,準確地說是生成token
* @param username
* @param secret
* @return
*/
public static String sign(String username, String secret){
try{
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
Algorithm algorithm = Algorithm.HMAC256(secret);
//附帶username,nickname資訊
return JWT.create().withClaim("username",username).withExpiresAt(date).sign(algorithm);
} catch (JWTCreationException e){
e.printStackTrace();
return null;
} catch (Exception e){
e.printStackTrace();
return null;
}
}
/**
* 獲取token的簽發時間
* @param token
* @return
*/
public static Date getIssueAt(String token){
try{
DecodedJWT jwt = JWT.decode(token);
return jwt.getIssuedAt();
} catch (JWTDecodeException e){
e.printStackTrace();
return null;
}
}
/**
* 驗證token是否過期
* @param token
* @return
*/
public static boolean isTokenExpired(String token){
Date now = Calendar.getInstance().getTime();
DecodedJWT jwt = JWT.decode(token);
return jwt.getExpiresAt().before(now);
}
/**
* 重繪token的有效期
* @param token
* @param secret
* @return
*/
public static String refreshTokenExpired(String token, String secret){
DecodedJWT jwt = JWT.decode(token); //決議token
Map<String, Claim> claims = jwt.getClaims(); //獲取token的引數資訊
try{
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
Algorithm algorithm = Algorithm.HMAC256(secret);
JWTCreator.Builder builder = JWT.create().withExpiresAt(date);
for(Map.Entry<String,Claim> entry : claims.entrySet()){
builder.withClaim(entry.getKey(),entry.getValue().asString());
}
return builder.sign(algorithm);
} catch (JWTCreationException | UnsupportedEncodingException e){
e.printStackTrace();
return null;
}
}
/**
* 生成16位隨機鹽,用在密碼加密上面
* @return
*/
public static String generateSalt(){
SecureRandomNumberGenerator secureRandomNumberGenerator = new SecureRandomNumberGenerator();
String hex = secureRandomNumberGenerator.nextBytes(16).toHex();
return hex;
}
}
定義JwtFilter
jwtfilter的作用故名思義,就是一個專門用來攔截含有token請求的過濾器,
對于前端發來的請求,這個過濾器都會進行過濾,具體是咱們的前置攔截處理和后置攔截處理分別回傳回應正常狀態和添加跨域請求,
除此以外,我們的isAccessAllowed()方法會驗證token的正確性,正確則繼續往下請求,如果驗證出錯則回傳錯誤資訊,拒絕訪問!【具體的流程大家可以康康代碼,我都有寫注釋的噢,花點時間的話還算好理解,講的話太長了,而我明天還有課啊啊啊】
package com.szu.filter;
import com.szu.shiro.token.JwtToken;
import com.szu.utils.JwtUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
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.PrintWriter;
/**
* 自定義的認證過濾器,用來攔截Header中攜帶token的請求
*/
@Component
public class JwtFilter extends BasicHttpAuthenticationFilter {
/**
* 前置攔截處理
* @param request
* @param response
* @return
* @throws Exception
*/
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
//servlet請求與回應的轉換
HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
//跨域時會首先發送一個option請求,這里我們給option請求直接回傳正常狀態
if(httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())){
httpServletResponse.setStatus(HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
}
/**
* 后置攔截處理
* @param request
* @param response
* @throws Exception
*/
@Override
protected void postHandle(ServletRequest request, ServletResponse response) throws Exception {
//添加跨域支持
this.fillCorsHeader(WebUtils.toHttp(request),WebUtils.toHttp(response));
}
/**
* 過濾器攔截請求的入口方法,所有請求都會進入該方法
* 回傳true則允許訪問
* @param request
* @param response
* @param mappedValue
* @return
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
// 原用來判斷是否是登錄請求,在本例中不會攔截登錄請求,用來檢測Header是否包含token欄位
if(this.isLoginRequest(request,response)){//看看原始碼,呼叫了isLoginAttempt()方法
return false; //回傳false后進入onAccessDenied()方法,回傳錯誤資訊
}
boolean allowed = false;
try{
//檢測header里的JWT Token內容是否正確,嘗試使用token進行登錄
allowed = this.executeLogin(request,response);
} catch (IllegalStateException e){ //未找到token
e.printStackTrace();
System.out.println("未找到token");
} catch (Exception e){
e.printStackTrace();
System.out.println("token檢驗出錯");
}
return allowed || super.isPermissive(mappedValue);
}
/**
* 檢測Header中是否包含token欄位
* @param request
* @param response
* @return
*/
@Override
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
return ((HttpServletRequest) request).getHeader(JwtUtils.AUTH_HEADER) == null;
}
/**
* 身份驗證,檢查JWT token是否合法
* @param request
* @param response
* @return
* @throws Exception
*/
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
//從請求頭里拿到token
AuthenticationToken token = createToken(request,response);
if(token == null){
String msg = "createToken method implementation returned null. A valid non-null AuthenticationToken must be created in order to execute a login attempt.";
throw new IllegalStateException(msg);
}
try{
Subject subject = getSubject(request,response);
subject.login(token); //讓shiro進行登錄驗證
//沒出錯則驗證成功
return onLoginSuccess(token,subject,request,response);
} catch (AuthenticationException e){
return onLoginFailure(token, e, request, response);
}
}
/**
* 從Header中提取 JWT token
* @param request
* @param response
* @return
*/
@Override
protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String authorization = httpServletRequest.getHeader(JwtUtils.AUTH_HEADER);
JwtToken token = new JwtToken(authorization);
return token;
}
/**
* isAccessAllowed()方法回傳false,會進入該方法,表示拒絕訪問
* @param request
* @param response
* @return
* @throws Exception
*/
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setContentType("application/json;charset=UTF-8");
httpServletResponse.setStatus(HttpStatus.UNAUTHORIZED.value());
PrintWriter writer = httpServletResponse.getWriter();
writer.write("{\"errorCode\":401,\"msg\":\"UNAUTHORIZED\"}");
fillCorsHeader(WebUtils.toHttp(request),httpServletResponse);
return false;
}
/**
* shiro利用 JWT Token 登錄成功后,進入該方法
* @param token
* @param subject
* @param request
* @param response
* @return
* @throws Exception
*/
@Override
protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception {
HttpServletResponse httpResponse = WebUtils.toHttp(response);
String newToken = null;
//登錄成功后重繪token
if(token instanceof JwtToken) {
newToken = JwtUtils.refreshTokenExpired(token.getCredentials().toString(),JwtUtils.SECRET);
}
if(newToken != null){
httpResponse.setHeader(JwtUtils.AUTH_HEADER,newToken);
}
return true;
}
/**
* 利用 JWT token 登錄失敗,會進入該方法
* @param token
* @param e
* @param request
* @param response
* @return
*/
@Override
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
return false;//直接回傳false,交給后面的 onAccessDenied()方法處理
}
//跨域請求的解決方案之一
protected void fillCorsHeader(HttpServletRequest request, HttpServletResponse response){
response.setHeader("Access-control-Allow-Origin", request.getHeader("Origin"));
response.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,HEAD");
response.setHeader("Access-Control-Allow-Headers",
request.getHeader("Access-Control-Request-Headers"));
}
}
定義JwtToken
JwtToken這個類實作了AuthenticationToken,如果我們將token作為引數構造出JwtToken物件的話,shiro的主體物件subject進行登錄驗證時就不用再new 一個UsernamePasswordToken 物件了,畢竟 JwtToken和UsernamePasswordToken 都繼承了同一個介面,
所以,說到這JwtToken的作用就顯而易見了,目的就是構造一個AuthenticationToken物件以供shiro登錄驗證
package com.szu.shiro.token;
import com.szu.utils.JwtUtils;
import org.apache.shiro.authc.AuthenticationToken;
/**
* 該類與UsernamePasswordToken差不多,都是AuthenticationToken介面的實作類
* 目的是封裝成UsernamePasswordToken讓shiro進行登錄、登出等操作
*/
public class JwtToken implements AuthenticationToken {
private static final long serialVersionUID = 1L;
//加密后的 JWT token
private String token;
private String username;
public JwtToken(String token){
this.token = token;
this.username = JwtUtils.getClaimFiled(token,"username");
}
@Override
public Object getPrincipal() {
return this.username;
}
@Override
public Object getCredentials() {
return token;
}
}
定義兩個Realm
ShiroRealm:其實是模擬的資料庫以及查詢
JwtRealm:這個自定義的realm就比較關鍵了,它實作了認證和授權的兩個方法,
認證的方法里面,我們獲取到JwtToken類的token后,獲取token里面的引數資訊(暫時只有username),然后查詢“資料庫”判斷,沒有則回傳錯誤資訊,即拋出例外,讓subject.login(token)所在的方法捕獲到例外進行處理,認證通過,即用戶名所對應的物件存在,則回傳SimpleAuthenticationInfo物件,讓請求能夠繼續請求loginController
授權的方法中,則是獲取到token攜帶的的username資訊來查詢其擁有的權限,然后進行設定即可,至此,我們的shiro作用就發揮得差不多了
[realm是由shiroConfig中的securityManager呼叫的]
JwtRealm.java
package com.szu.shiro.realm;
import com.szu.db.ShiroRealm;
import com.szu.entity.User;
import com.szu.entity.UserEntity;
import com.szu.mapper.UserMapper;
import com.szu.shiro.token.JwtToken;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AccountException;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UnknownAccountException;
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.stereotype.Component;
import java.util.Set;
@Component
public class JwtRealm extends AuthorizingRealm {
/**
* 限定這個 Realm 只處理我們自定義的 JwtToken
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
/**
* 此處的 SimpleAuthenticationInfo 可回傳任意值,密碼校驗時不會用到它
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken)
throws AuthenticationException {
JwtToken jwtToken = (JwtToken) authcToken;
if (jwtToken.getPrincipal() == null) {
throw new AccountException("JWT token引數例外!");
}
// 從 JwtToken 中獲取當前用戶
String username = jwtToken.getPrincipal().toString();
// 查詢資料庫獲取用戶資訊,此處使用 Map 來模擬資料庫
UserEntity user = ShiroRealm.userMap.get(username);
// 用戶不存在
if (user == null) {
throw new UnknownAccountException("用戶不存在!");
}
// 用戶被鎖定
if (user.getLocked()) {
throw new LockedAccountException("該用戶已被鎖定,暫時無法登錄!");
}
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, username, getName());
return info;
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// 獲取當前用戶
UserEntity currentUser = (UserEntity) SecurityUtils.getSubject().getPrincipal();
// UserEntity currentUser = (UserEntity) principals.getPrimaryPrincipal();
// 查詢資料庫,獲取用戶的角色資訊
Set<String> roles = ShiroRealm.roleMap.get(currentUser.getName());
// 查詢資料庫,獲取用戶的權限資訊
Set<String> perms = ShiroRealm.permMap.get(currentUser.getName());
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.setRoles(roles);
info.setStringPermissions(perms);
return info;
}
}
ShiroRealm.java
此處存盤的密碼都是被shiro加密過的
String password = new SimpleHash("MD5", user.getPassword(), user.getUserName()
package com.szu.db;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import com.szu.entity.UserEntity;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
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;
/**
* 同時開啟身份驗證和權限驗證,需要繼承 AuthorizingRealm
* 并實作其 doGetAuthenticationInfo()和 doGetAuthorizationInfo 兩個方法
*/
@SuppressWarnings("serial")
public class ShiroRealm extends AuthorizingRealm {
public static Map<String, UserEntity> userMap = new HashMap<String, UserEntity>(16);
public static Map<String, Set<String>> roleMap = new HashMap<String, Set<String>>(16);
public static Map<String, Set<String>> permMap = new HashMap<String, Set<String>>(16);
static {
UserEntity user1 = new UserEntity(1L, "gorho", "dd524c4c66076d1fa07e1fa1c94a91233772d132", "灰先生", false);
UserEntity user2 = new UserEntity(2L, "plum", "cce369436bbb9f0325689a3a6d5d6b9b8a3f39a0", "李先生", false);
userMap.put("gorho", user1);
userMap.put("plum", user2);
roleMap.put("gorho", new HashSet<String>() {
{
add("admin");
}
});
roleMap.put("plum", new HashSet<String>() {
{
add("guest");
}
});
permMap.put("plum", new HashSet<String>() {
{
add("article:read");
}
});
}
/**
* 限定這個 Realm 只處理 UsernamePasswordToken
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof UsernamePasswordToken;
}
/**
* 查詢資料庫,將獲取到的用戶安全資料封裝回傳
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 從 AuthenticationToken 中獲取當前用戶
String username = (String) token.getPrincipal();
// 查詢資料庫獲取用戶資訊,此處使用 Map 來模擬資料庫
UserEntity user = userMap.get(username);
// 用戶不存在
if (user == null) {
throw new UnknownAccountException("用戶不存在!");
}
// 用戶被鎖定
if (user.getLocked()) {
throw new LockedAccountException("該用戶已被鎖定,暫時無法登錄!");
}
// 使用用戶名作為鹽值
ByteSource credentialsSalt = ByteSource.Util.bytes(username);
/**
* 將獲取到的用戶資料封裝成 AuthenticationInfo 物件回傳,此處封裝為 SimpleAuthenticationInfo 物件,
* 引數1. 認證的物體資訊,可以是從資料庫中獲取到的用戶物體類物件或者用戶名
* 引數2. 查詢獲取到的登錄密碼
* 引數3. 鹽值
* 引數4. 當前 Realm 物件的名稱,直接呼叫父類的 getName() 方法即可
*/
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, user.getPassword(), credentialsSalt,
getName());
return info;
}
/**
* 查詢資料庫,將獲取到的用戶的角色及權限資訊回傳
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// 獲取當前用戶
UserEntity currentUser = (UserEntity) SecurityUtils.getSubject().getPrincipal();
// UserEntity currentUser = (UserEntity)principals.getPrimaryPrincipal();
// 查詢資料庫,獲取用戶的角色資訊
Set<String> roles = roleMap.get(currentUser.getName());
// 查詢資料庫,獲取用戶的權限資訊
Set<String> perms = permMap.get(currentUser.getName());
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.setRoles(roles);
info.setStringPermissions(perms);
return info;
}
}
兩個"工具人"
JwtCredentialsMatcher.java
該類的主要作用就是驗證JwtToken的內容是否合法,是realm的憑證校驗器,不自定義也可以的,換成HashedCredentialsMatcher并設定加密方式,散列次數什么的就行了
package com.szu.utils;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.credential.CredentialsMatcher;
import org.springframework.stereotype.Component;
@Component
public class JwtCredentialsMatcher implements CredentialsMatcher {
/**
* JwtCredentialsMatcher只需驗證JwtToken內容是否合法
*/
@Override
public boolean doCredentialsMatch(AuthenticationToken authenticationToken, AuthenticationInfo authenticationInfo) {
String token = authenticationToken.getCredentials().toString();
String username = authenticationToken.getPrincipal().toString();
try {
Algorithm algorithm = Algorithm.HMAC256(JwtUtils.SECRET);
JWTVerifier verifier = JWT.require(algorithm).withClaim("username", username).build();
verifier.verify(token);
return true;
} catch (JWTVerificationException e) {
e.printStackTrace();
} catch (Exception e){
e.printStackTrace();
}
return false;
}
}
MultiRealmAuthenticator.java
MultiRealmAuthenticator 用來解決Shiro中出現的具體的認證例外無法正常回傳,僅回傳父類 AuthenticationException 的問題,
package com.szu.shiro.realm;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.pam.AuthenticationStrategy;
import org.apache.shiro.authc.pam.ModularRealmAuthenticator;
import org.apache.shiro.realm.Realm;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.util.Collection;
@Component
public class MultiRealmAuthenticator extends ModularRealmAuthenticator {
private static final Logger log = LoggerFactory.getLogger(MultiRealmAuthenticator.class);
@Override
protected AuthenticationInfo doMultiRealmAuthentication(Collection<Realm> realms, AuthenticationToken token)
throws AuthenticationException {
AuthenticationStrategy strategy = getAuthenticationStrategy();
AuthenticationInfo aggregate = strategy.beforeAllAttempts(realms, token);
if (log.isTraceEnabled()) {
log.trace("Iterating through {} realms for PAM authentication", realms.size());
}
AuthenticationException authenticationException = null;
for (Realm realm : realms) {
aggregate = strategy.beforeAttempt(realm, token, aggregate);
if (realm.supports(token)) {
log.trace("Attempting to authenticate token [{}] using realm [{}]", token, realm);
AuthenticationInfo info = null;
try {
info = realm.getAuthenticationInfo(token);
} catch (AuthenticationException e) {
authenticationException = e;
if (log.isDebugEnabled()) {
String msg = "Realm [" + realm
+ "] threw an exception during a multi-realm authentication attempt:";
log.debug(msg, e);
}
}
aggregate = strategy.afterAttempt(realm, token, info, aggregate, authenticationException);
} else {
log.debug("Realm [{}] does not support token {}. Skipping realm.", realm, token);
}
}
if (authenticationException != null) {
throw authenticationException;
}
aggregate = strategy.afterAllAttempts(token, aggregate);
return aggregate;
}
}
全域例外處理
這個類主要是用來捕獲例外并回傳不同例外的錯誤資訊,方便前端判斷并處理,但是這里只是簡單提了,具體的教程我會以后寫一篇的哈,大家先有這個前后端分離的概念就行
package com.szu.exception;
import com.szu.entity.dto.BaseResponse;
import org.apache.shiro.ShiroException;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import javax.servlet.http.HttpServletRequest;
@RestControllerAdvice
public class MyExceptionHandler {
// 捕捉shiro的例外
@ExceptionHandler(ShiroException.class)
public Object handleShiroException(ShiroException e) {
BaseResponse<Object> ret = new BaseResponse<Object>();
ret.setErrCode(401);
ret.setMsg(e.getMessage());
return ret;
}
// 捕捉其他所有例外
@ExceptionHandler(Exception.class)
public Object globalException(HttpServletRequest request, Throwable ex) {
BaseResponse<Object> ret = new BaseResponse<Object>();
ret.setErrCode(401);
ret.setMsg(ex.getMessage());
return ret;
}
}
兩個controller
LoginController.java
登錄的控制器,在controller中用shiro進行登錄的驗證判斷
該控制器方法主要想體現身份的權限控制
package com.szu.controller;
import com.szu.entity.dto.BaseResponse;
import com.szu.utils.JwtUtils;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.subject.Subject;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;
@RestController
public class LoginController {
@PostMapping(value = "/login")
public Object userLogin(@RequestParam(name = "username", required = true) String userName,
@RequestParam(name = "password", required = true) String password, ServletResponse response) {
// 獲取當前用戶主體
Subject subject = SecurityUtils.getSubject();
String msg = null;
boolean loginSuccess = false;
// 將用戶名和密碼封裝成 UsernamePasswordToken 物件
UsernamePasswordToken token = new UsernamePasswordToken(userName, password);
try {
subject.login(token);
msg = "登錄成功,";
loginSuccess = true;
} catch (UnknownAccountException uae) { // 賬號不存在
msg = "用戶名與密碼不匹配,請檢查后重新輸入!";
} catch (IncorrectCredentialsException ice) { // 賬號與密碼不匹配
msg = "用戶名與密碼不匹配,請檢查后重新輸入!";
} catch (LockedAccountException lae) { // 賬號已被鎖定
msg = "該賬戶已被鎖定,如需解鎖請聯系管理員!";
} catch (AuthenticationException ae) { // 其他身份驗證例外
msg = "登錄例外,請聯系管理員!";
}
BaseResponse<Object> ret = new BaseResponse<Object>();
if (loginSuccess) {
// 若登錄成功,簽發 JWT token
String jwtToken = JwtUtils.sign(userName, JwtUtils.SECRET);
// 將簽發的 JWT token 設定到 HttpServletResponse 的 Header 中
((HttpServletResponse) response).setHeader(JwtUtils.AUTH_HEADER, jwtToken);
//
ret.setErrCode(0);
ret.setMsg(msg);
return ret;
} else {
ret.setErrCode(401);
ret.setMsg(msg);
return ret;
}
}
@GetMapping("/logout")
public Object logout() {
BaseResponse<Object> ret = new BaseResponse<Object>();
ret.setErrCode(0);
ret.setMsg("退出登錄");
return ret;
}
}
ArticleController.java
這個控制器類方法可以在用戶登錄后回傳簡單的示意資訊
主要想體現對用戶權限的控制
package com.szu.controller;
import com.szu.entity.dto.BaseResponse;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/article")
public class ArticleController {
@GetMapping("/delete")
@RequiresRoles(value = { "admin" })
public Object deleteArticle(ModelMap model) {
BaseResponse<Object> ret = new BaseResponse<Object>();
ret.setErrCode(0);
ret.setMsg("文章洗掉成功!");
return ret;
}
@GetMapping("/read")
@RequiresPermissions(value = { "article:read" })
public Object readArticle(ModelMap model) {
BaseResponse<Object> ret = new BaseResponse<Object>();
ret.setErrCode(0);
ret.setMsg("請您鑒賞!");
return ret;
}
}
介面測驗
啟動springboot后,任意選一個你喜歡的介面測驗工具,首先進行登錄測驗
如圖,post請求登錄

回傳結果如下:
注意,左側的請求頭里面出現了我們的token資料,說明咱們的代碼沒有白寫(crtl c+v)

然后復制我們的token,加到請求頭中,我們就可以在時效內訪問文章啦【電腦要沒電了,剩下的不演示了】
回傳成功!!!

兄弟們,這就是簡單的前后端分離的shiro+jwt的鑒權控制了,代碼只要復制就能跑的,大家不妨嘗試一下!!重要地方都有注釋
如果有用的話記得給文章點贊、收藏、轉發哈,下一篇我們講《前后端分離之全域例外處理并回傳狀態碼》或者 《mybatis-plus的crud》
文章參考自https://blog.csdn.net/pengjunlee/article/details/95600843!良心教程,幫助我成功理解了!!
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/278808.html
標籤:其他
上一篇:告別DNS劫持,一文讀懂DoH
下一篇:CSRF:你的身邊安全嗎
