詳細請查看https://zhuanlan.zhihu.com/p/391839846
1. 概述
1.1 SpringBoot
這個就沒什么好說的了,能看到這個教程的,估計都是可以說精通了SpringBoot
的使用
1.2 Shiro
一個安全框架,但不只是一個安全框架。它能實現多種多樣的功能。并不只是局限在web層。在國內的市場份額占比高于SpringSecurity
,是使用最多的安全框架
可以實現用戶的認證和授權。比SpringSecurity
要簡單的多。
1.3 Jwt
我的理解就是可以進行客戶端與服務端之間驗證的一種技術,取代了之前使用Session來驗證的不安全性
為什么不適用Session?
原理是,登錄之后客戶端和服務端各自保存一個相應的SessionId,每次客戶端發起請求的時候就得攜帶這個SessionId來進行比對
- Session在用戶請求量大的時候服務器開銷太大了
- Session不利于搭建服務器的集群(也就是必須訪問原本的那個服務器才能獲取對應的SessionId)
它使用的是一種令牌技術
Jwt字符串分為三部分
-
Header
存儲兩個變量
- 秘鑰(可以用來比對)
- 算法(也就是下面將Header和payload加密成Signature)
-
payload
存儲很多東西,基礎信息有如下幾個
- 簽發人,也就是這個“令牌”歸屬于哪個用戶。一般是
userId
- 創建時間,也就是這個令牌是什么時候創建的
- 失效時間,也就是這個令牌什么時候失效
- 唯一標識,一般可以使用算法生成一個唯一標識
- 簽發人,也就是這個“令牌”歸屬于哪個用戶。一般是
-
Signature
這個是上面兩個經過Header中的算法加密生成的,用于比對信息,防止篡改Header和payload
然后將這三個部分的信息經過加密生成一個JwtToken
的字符串,發送給客戶端,客戶端保存在本地。當客戶端發起請求的時候攜帶這個到服務端(可以是在cookie
,可以是在header
,可以是在localStorage
中),在服務端進行驗證
好了,廢話不多說了,下面開始實戰,實戰分為以下幾個部分
SpringBoot
整合Shiro
SpringBoot
整合Jwt
SpringBoot
+Shiro
+Jwt
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.11.0</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
2. SpringBoot整合Shiro
兩種方式:
- 將ssm的整合的配置使用java代碼方式在springBoot中寫一遍
- 使用官方提供的start
2.1 使用start整合springBoot
pom.xml
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.4.0</version>
</dependency>
<!--注意不要寫成shiro-spring-boot-starter-->
application.properties
shiro.loginUrl="xxx"
#認證不通過的頁面
shiro.UnauthorizedUrl="xxx"
#授權不通過的跳轉頁面
創建ShiroConfig.java進行一些簡單的配置
@Configuration
public class SpringShiroConfig {
@Bean
public Realm customRealm() {
return new CustomRealm();
}
@Bean
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(customRealm());
// 關閉 ShiroDAO 功能
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
// 不需要將 Shiro Session 中的東西存到任何地方(包括 Http Session 中)
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);
return securityManager;
}
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition chain = new DefaultShiroFilterChainDefinition();
// 哪些請求可以匿名訪問
chain.addPathDefinition("/login", "anon"); // 登錄接口
chain.addPathDefinition("/notLogin", "anon"); // 未登錄錯誤提示接口
chain.addPathDefinition("/403", "anon"); // 權限不足錯誤提示接口
// 除了以上的請求外,其它請求都需要登錄
chain.addPathDefinition("/**", "authc");
return chain;
}
// Shiro 和 Spring AOP 整合時的特殊設置
@Bean
public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
creator.setProxyTargetClass(true);
return creator;
}
}
//還有關閉ShiroDao功能
創建自定義的Realm
public class CustomRealm extends AuthorizingRealm {
private static final Set<String> tomRoleNameSet = new HashSet<>();
private static final Set<String> tomPermissionNameSet = new HashSet<>();
private static final Set<String> jerryRoleNameSet = new HashSet<>();
private static final Set<String> jerryPermissionNameSet = new HashSet<>();
static {
tomRoleNameSet.add("admin");
jerryRoleNameSet.add("user");
tomPermissionNameSet.add("user:insert");
tomPermissionNameSet.add("user:update");
tomPermissionNameSet.add("user:delete");
tomPermissionNameSet.add("user:query");
jerryPermissionNameSet.add("user:query");
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String username = (String) principals.getPrimaryPrincipal();
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
if (username.equals("tom")) {
info.addRoles(tomRoleNameSet);
info.addStringPermissions(tomPermissionNameSet);
} else if (username.equals("jerry")) {
info.addRoles(jerryRoleNameSet);
info.addStringPermissions(jerryPermissionNameSet);
}
return info;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String username = (String) token.getPrincipal();
if (username == null)
throw new UnknownAccountException("用戶名不能為空");
SimpleAuthenticationInfo info = null;
if (username.equals("tom"))
return new SimpleAuthenticationInfo("tom", "123", CustomRealm.class.getName());
else if (username.equals("jerry"))
return new SimpleAuthenticationInfo("jerry", "123", CustomRealm.class.getName());
else
return null;
}
}
2.2 不使用starter
<!-- 自動依賴導入 shiro-core 和 shiro-web -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.1</version>
</dependency>
編寫 Shiro 的配置類:ShiroConfig
將 Shiro 的配置信息(spring-shiro.xml 和 spring-web.xml)以 Java 代碼配置的形式改寫:
@Configuration
public class ShiroConfig {
@Bean
public Realm realm() {
return new CustomRealm();
}
@Bean
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(realm());
return securityManager;
}
@Bean
public ShiroFilterFactoryBean shirFilter() {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager());
shiroFilterFactoryBean.setLoginUrl("/loginPage");
shiroFilterFactoryBean.setUnauthorizedUrl("/403");
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
filterChainDefinitionMap.put("/loginPage", "anon");
filterChainDefinitionMap.put("/403", "anon");
filterChainDefinitionMap.put("/login", "anon");
filterChainDefinitionMap.put("/hello", "anon");
filterChainDefinitionMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
/* ################################################################# */
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
// 強制指定注解的底層實現使用 cglib 方案
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
}
編寫 Controller
與 Shiro 和 SSM 的整合一樣。略
編寫 Thymeleaf 頁面
略
3. SpringBoot整合Jwt
3.1 依賴
1. springboot
2. java-jwt--核心依賴
3. jjwt--java版本的輔助幫助模塊
3.2 代碼
-
創建JwtUtil
package cn.coderymy.utils; import java.util.*; import com.auth0.jwt.*; import com.auth0.jwt.algorithms.Algorithm; import io.jsonwebtoken.*; import org.apache.commons.codec.binary.Base64; import java.util.*; public class JwtUtil { // 生成簽名是所使用的秘鑰 private final String base64EncodedSecretKey; // 生成簽名的時候所使用的加密算法 private final SignatureAlgorithm signatureAlgorithm; public JwtUtil(String secretKey, SignatureAlgorithm signatureAlgorithm) { this.base64EncodedSecretKey = Base64.encodeBase64String(secretKey.getBytes()); this.signatureAlgorithm = signatureAlgorithm; } /** * 生成 JWT Token 字符串 * * @param iss 簽發人名稱 * @param ttlMillis jwt 過期時間 * @param claims 額外添加到荷部分的信息。 * 例如可以添加用戶名、用戶ID、用戶(加密前的)密碼等信息 */ public String encode(String iss, long ttlMillis, Map<String, Object> claims) { if (claims == null) { claims = new HashMap<>(); } // 簽發時間(iat):荷載部分的標準字段之一 long nowMillis = System.currentTimeMillis(); Date now = new Date(nowMillis); // 下面就是在為payload添加各種標準聲明和私有聲明了 JwtBuilder builder = Jwts.builder() // 荷載部分的非標準字段/附加字段,一般寫在標準的字段之前。 .setClaims(claims) // JWT ID(jti):荷載部分的標準字段之一,JWT 的唯一性標識,雖不強求,但盡量確保其唯一性。 .setId(UUID.randomUUID().toString()) // 簽發時間(iat):荷載部分的標準字段之一,代表這個 JWT 的生成時間。 .setIssuedAt(now) // 簽發人(iss):荷載部分的標準字段之一,代表這個 JWT 的所有者。通常是 username、userid 這樣具有用戶代表性的內容。 .setSubject(iss) // 設置生成簽名的算法和秘鑰 .signWith(signatureAlgorithm, base64EncodedSecretKey); if (ttlMillis >= 0) { long expMillis = nowMillis + ttlMillis; Date exp = new Date(expMillis); // 過期時間(exp):荷載部分的標準字段之一,代表這個 JWT 的有效期。 builder.setExpiration(exp); } return builder.compact(); } /** * JWT Token 由 頭部 荷載部 和 簽名部 三部分組成。簽名部分是由加密算法生成,無法反向解密。 * 而 頭部 和 荷載部分是由 Base64 編碼算法生成,是可以反向反編碼回原樣的。 * 這也是為什么不要在 JWT Token 中放敏感數據的原因。 * * @param jwtToken 加密后的token * @return claims 返回荷載部分的鍵值對 */ public Claims decode(String jwtToken) { // 得到 DefaultJwtParser return Jwts.parser() // 設置簽名的秘鑰 .setSigningKey(base64EncodedSecretKey) // 設置需要解析的 jwt .parseClaimsJws(jwtToken) .getBody(); } /** * 校驗 token * 在這里可以使用官方的校驗,或, * 自定義校驗規則,例如在 token 中攜帶密碼,進行加密處理后和數據庫中的加密密碼比較。 * * @param jwtToken 被校驗的 jwt Token */ public boolean isVerify(String jwtToken) { Algorithm algorithm = null; switch (signatureAlgorithm) { case HS256: algorithm = Algorithm.HMAC256(Base64.decodeBase64(base64EncodedSecretKey)); break; default: throw new RuntimeException("不支持該算法"); } JWTVerifier verifier = JWT.require(algorithm).build(); verifier.verify(jwtToken); // 校驗不通過會拋出異常 /* // 得到DefaultJwtParser Claims claims = decode(jwtToken); if (claims.get("password").equals(user.get("password"))) { return true; } */ return true; } public static void main(String[] args) { JwtUtil util = new JwtUtil("tom", SignatureAlgorithm.HS256); Map<String, Object> map = new HashMap<>(); map.put("username", "tom"); map.put("password", "123456"); map.put("age", 20); String jwtToken = util.encode("tom", 30000, map); System.out.println(jwtToken); /* util.isVerify(jwtToken); System.out.println("合法"); */ util.decode(jwtToken).entrySet().forEach((entry) -> { System.out.println(entry.getKey() + ": " + entry.getValue()); }); } }
<font color="yellow">解析:</font>
- <font color="red">在創建JwtUtil對象的時候需要傳入幾個數值</font>
- 這個用戶,用來生成秘鑰
- 這個加密算法,用來加密生成jwt
- 通過jwt數據獲取用戶信息的方法(decode())
- 判斷jwt是否存在或者過期的方法
- 最后是測試方法
- <font color="red">在創建JwtUtil對象的時候需要傳入幾個數值</font>
-
創建一個Controller
- 登錄的Controller
- 獲取username和password,進行與數據庫的校驗,校驗成功執行下一步,失敗直接返回
- 使用創建JwtUtil對象,傳入username和需要使用的加密算法
- 創建需要加在載荷中的一些基本信息的一個map對象
- 創建jwt數據,傳入username,保存時間,以及基本信息的map對象
- 校驗Controller
- 獲取前臺傳入的Jwt數據
- 使用
JWTUtil
中的isVerify
進行該jwt數據有效的校驗
- 登錄的Controller
4. SpringBoot+Shiro+Jwt
-
由于需要對shiro的SecurityManager進行設置,所以不能使用shiro-spring-boot-starter進行與springboot的整合,只能使用spring-shiro
<!-- 自動依賴導入 shiro-core 和 shiro-web --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.4.1</version> </dependency>
-
由于需要實現無狀態的web,所以使用不到Shiro的Session功能,嚴謹點就是將其關閉
public class JwtDefaultSubjectFactory extends DefaultWebSubjectFactory { @Override public Subject createSubject(SubjectContext context) { // 不創建 session context.setSessionCreationEnabled(false); return super.createSubject(context); } }
這樣如果調用
getSession()
方法會拋出異常
4.1 流程
- 用戶請求,不攜帶token,就在JwtFilter處拋出異常/返回沒有登錄,讓它去登陸
- 用戶請求,攜帶token,就到JwtFilter中獲取jwt,封裝成JwtToken對象。然后使用JwtRealm進行認證
- 在JwtRealm中進行認證判斷這個token是否有效,也就是
執行流程:1. 客戶端發起請求,shiro的過濾器生效,判斷是否是login或logout的請求<br/> 如果是就直接執行請求<br/> 如果不是就進入JwtFilter2. JwtFilter執行流程 1. 獲取header是否有"Authorization"的鍵,有就獲取,沒有就拋出異常 2. 將獲取的jwt字符串封裝在創建的JwtToken中,使用subject執行login()方法進行校驗。這個方法會調用創建的JwtRealm 3. 執行JwtRealm中的認證方法,使用`jwtUtil.isVerify(jwt)`判斷是否登錄過 4. 返回true就使基礎執行下去
4.2 快速開始
0. JwtDeafultSubjectFactory
package cn.coderymy.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);
}
}
1. 創建JwtUtil
這個一般是固定的寫法,其中寫了大量注釋
package cn.coderymy.util;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.apache.commons.codec.binary.Base64;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/*
* 總的來說,工具類中有三個方法
* 獲取JwtToken,獲取JwtToken中封裝的信息,判斷JwtToken是否存在
* 1. encode(),參數是=簽發人,存在時間,一些其他的信息=。返回值是JwtToken對應的字符串
* 2. decode(),參數是=JwtToken=。返回值是荷載部分的鍵值對
* 3. isVerify(),參數是=JwtToken=。返回值是這個JwtToken是否存在
* */
public class JwtUtil {
//創建默認的秘鑰和算法,供無參的構造方法使用
private static final String defaultbase64EncodedSecretKey = "badbabe";
private static final SignatureAlgorithm defaultsignatureAlgorithm = SignatureAlgorithm.HS256;
public JwtUtil() {
this(defaultbase64EncodedSecretKey, defaultsignatureAlgorithm);
}
private final String base64EncodedSecretKey;
private final SignatureAlgorithm signatureAlgorithm;
public JwtUtil(String secretKey, SignatureAlgorithm signatureAlgorithm) {
this.base64EncodedSecretKey = Base64.encodeBase64String(secretKey.getBytes());
this.signatureAlgorithm = signatureAlgorithm;
}
/*
*這里就是產生jwt字符串的地方
* jwt字符串包括三個部分
* 1. header
* -當前字符串的類型,一般都是“JWT”
* -哪種算法加密,“HS256”或者其他的加密算法
* 所以一般都是固定的,沒有什么變化
* 2. payload
* 一般有四個最常見的標準字段(下面有)
* iat:簽發時間,也就是這個jwt什么時候生成的
* jti:JWT的唯一標識
* iss:簽發人,一般都是username或者userId
* exp:過期時間
*
* */
public String encode(String iss, long ttlMillis, Map<String, Object> claims) {
//iss簽發人,ttlMillis生存時間,claims是指還想要在jwt中存儲的一些非隱私信息
if (claims == null) {
claims = new HashMap<>();
}
long nowMillis = System.currentTimeMillis();
JwtBuilder builder = Jwts.builder()
.setClaims(claims)
.setId(UUID.randomUUID().toString())//2. 這個是JWT的唯一標識,一般設置成唯一的,這個方法可以生成唯一標識
.setIssuedAt(new Date(nowMillis))//1. 這個地方就是以毫秒為單位,換算當前系統時間生成的iat
.setSubject(iss)//3. 簽發人,也就是JWT是給誰的(邏輯上一般都是username或者userId)
.signWith(signatureAlgorithm, base64EncodedSecretKey);//這個地方是生成jwt使用的算法和秘鑰
if (ttlMillis >= 0) {
long expMillis = nowMillis + ttlMillis;
Date exp = new Date(expMillis);//4. 過期時間,這個也是使用毫秒生成的,使用當前時間+前面傳入的持續時間生成
builder.setExpiration(exp);
}
return builder.compact();
}
//相當于encode的方向,傳入jwtToken生成對應的username和password等字段。Claim就是一個map
//也就是拿到荷載部分所有的鍵值對
public Claims decode(String jwtToken) {
// 得到 DefaultJwtParser
return Jwts.parser()
// 設置簽名的秘鑰
.setSigningKey(base64EncodedSecretKey)
// 設置需要解析的 jwt
.parseClaimsJws(jwtToken)
.getBody();
}
//判斷jwtToken是否合法
public boolean isVerify(String jwtToken) {
//這個是官方的校驗規則,這里只寫了一個”校驗算法“,可以自己加
Algorithm algorithm = null;
switch (signatureAlgorithm) {
case HS256:
algorithm = Algorithm.HMAC256(Base64.decodeBase64(base64EncodedSecretKey));
break;
default:
throw new RuntimeException("不支持該算法");
}
JWTVerifier verifier = JWT.require(algorithm).build();
verifier.verify(jwtToken); // 校驗不通過會拋出異常
//判斷合法的標準:1. 頭部和荷載部分沒有篡改過。2. 沒有過期
return true;
}
public static void main(String[] args) {
JwtUtil util = new JwtUtil("tom", SignatureAlgorithm.HS256);
//以tom作為秘鑰,以HS256加密
Map<String, Object> map = new HashMap<>();
map.put("username", "tom");
map.put("password", "123456");
map.put("age", 20);
String jwtToken = util.encode("tom", 30000, map);
System.out.println(jwtToken);
util.decode(jwtToken).entrySet().forEach((entry) -> {
System.out.println(entry.getKey() + ": " + entry.getValue());
});
}
}
2. 創建JwtFilter
也就是在Shiro的攔截器中多加一個,等下需要在配置文件中注冊這個過濾器
package cn.coderymy.filter;
import cn.coderymy.shiro.JwtToken;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.web.filter.AccessControlFilter;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/*
* 自定義一個Filter,用來攔截所有的請求判斷是否攜帶Token
* isAccessAllowed()判斷是否攜帶了有效的JwtToken
* onAccessDenied()是沒有攜帶JwtToken的時候進行賬號密碼登錄,登錄成功允許訪問,登錄失敗拒絕訪問
* */
@Slf4j
public class JwtFilter extends AccessControlFilter {
/*
* 1. 返回true,shiro就直接允許訪問url
* 2. 返回false,shiro才會根據onAccessDenied的方法的返回值決定是否允許訪問url
* */
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
log.warn("isAccessAllowed 方法被調用");
//這里先讓它始終返回false來使用onAccessDenied()方法
return false;
}
/**
* 返回結果為true表明登錄通過
*/
@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
log.warn("onAccessDenied 方法被調用");
//這個地方和前端約定,要求前端將jwtToken放在請求的Header部分
//所以以后發起請求的時候就需要在Header中放一個Authorization,值就是對應的Token
HttpServletRequest request = (HttpServletRequest) servletRequest;
String jwt = request.getHeader("Authorization");
log.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();
onLoginFail(servletResponse);
//調用下面的方法向客戶端返回錯誤信息
return false;
}
return true;
//執行方法中沒有拋出異常就表示登錄成功
}
//登錄失敗時默認返回 401 狀態碼
private void onLoginFail(ServletResponse response) throws IOException {
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
httpResponse.getWriter().write("login error");
}
}
3. 創建JwtToken
其中封裝了需要傳遞的jwt
字符串
package cn.coderymy.shiro;
import org.apache.shiro.authc.AuthenticationToken;
//這個就類似UsernamePasswordToken
public class JwtToken implements AuthenticationToken {
private String jwt;
public JwtToken(String jwt) {
this.jwt = jwt;
}
@Override//類似是用戶名
public Object getPrincipal() {
return jwt;
}
@Override//類似密碼
public Object getCredentials() {
return jwt;
}
//返回的都是jwt
}
4. JwtRealm
創建判斷jwt
是否有效的認證方式的Realm
package cn.coderymy.realm;
import cn.coderymy.shiro.JwtToken;
import cn.coderymy.util.JwtUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
@Slf4j
public class JwtRealm extends AuthorizingRealm {
/*
* 多重寫一個support
* 標識這個Realm是專門用來驗證JwtToken
* 不負責驗證其他的token(UsernamePasswordToken)
* */
@Override
public boolean supports(AuthenticationToken token) {
//這個token就是從過濾器中傳入的jwtToken
return token instanceof JwtToken;
}
//授權
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
//認證
//這個token就是從過濾器中傳入的jwtToken
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String jwt = (String) token.getPrincipal();
if (jwt == null) {
throw new NullPointerException("jwtToken 不允許為空");
}
//判斷
JwtUtil jwtUtil = new JwtUtil();
if (!jwtUtil.isVerify(jwt)) {
throw new UnknownAccountException();
}
//下面是驗證這個user是否是真實存在的
String username = (String) jwtUtil.decode(jwt).get("username");//判斷數據庫中username是否存在
log.info("在使用token登錄"+username);
return new SimpleAuthenticationInfo(jwt,jwt,"JwtRealm");
//這里返回的是類似賬號密碼的東西,但是jwtToken都是jwt字符串。還需要一個該Realm的類名
}
}
5. ShiroConfig
配置一些信息
- 因為不適用Session,所以為了防止會調用getSession()方法而產生錯誤,所以默認調用自定義的Subject方法
- 一些修改,關閉SHiroDao等
- 注冊JwtFilter
package cn.coderymy.config;
import cn.coderymy.filter.JwtFilter;
import cn.coderymy.realm.JwtRealm;
import cn.coderymy.shiro.JwtDefaultSubjectFactory;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.mgt.SubjectFactory;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.filter.authc.AnonymousFilter;
import org.apache.shiro.web.filter.authc.LogoutFilter;
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.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
//springBoot整合jwt實現認證有三個不一樣的地方,對應下面abc
@Configuration
public class ShiroConfig {
/*
* a. 告訴shiro不要使用默認的DefaultSubject創建對象,因為不能創建Session
* */
@Bean
public SubjectFactory subjectFactory() {
return new JwtDefaultSubjectFactory();
}
@Bean
public Realm realm() {
return new JwtRealm();
}
@Bean
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(realm());
/*
* b
* */
// 關閉 ShiroDAO 功能
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
// 不需要將 Shiro Session 中的東西存到任何地方(包括 Http Session 中)
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);
//禁止Subject的getSession方法
securityManager.setSubjectFactory(subjectFactory());
return securityManager;
}
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean() {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager());
shiroFilter.setLoginUrl("/unauthenticated");
shiroFilter.setUnauthorizedUrl("/unauthorized");
/*
* c. 添加jwt過濾器,并在下面注冊
* 也就是將jwtFilter注冊到shiro的Filter中
* 指定除了login和logout之外的請求都先經過jwtFilter
* */
Map<String, Filter> filterMap = new HashMap<>();
//這個地方其實另外兩個filter可以不設置,默認就是
filterMap.put("anon", new AnonymousFilter());
filterMap.put("jwt", new JwtFilter());
filterMap.put("logout", new LogoutFilter());
shiroFilter.setFilters(filterMap);
// 攔截器
Map<String, String> filterRuleMap = new LinkedHashMap<>();
filterRuleMap.put("/login", "anon");
filterRuleMap.put("/logout", "logout");
filterRuleMap.put("/**", "jwt");
shiroFilter.setFilterChainDefinitionMap(filterRuleMap);
return shiroFilter;
}
}
6. 測試
package cn.coderymy.controller;
import cn.coderymy.util.JwtUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@Controller
public class LoginController {
@RequestMapping("/login")
public ResponseEntity<Map<String, String>> login(String username, String password) {
log.info("username:{},password:{}",username,password);
Map<String, String> map = new HashMap<>();
if (!"tom".equals(username) || !"123".equals(password)) {
map.put("msg", "用戶名密碼錯誤");
return ResponseEntity.ok(map);
}
JwtUtil jwtUtil = new JwtUtil();
Map<String, Object> chaim = new HashMap<>();
chaim.put("username", username);
String jwtToken = jwtUtil.encode(username, 5 * 60 * 1000, chaim);
map.put("msg", "登錄成功");
map.put("token", jwtToken);
return ResponseEntity.ok(map);
}
@RequestMapping("/testdemo")
public ResponseEntity<String> testdemo() {
return ResponseEntity.ok("我愛蛋炒飯");
}
}
4.3 授權方面的信息
在JwtRealm中的授權部分,可以使用JwtUtil.decode(jwt).get("username")
獲取到username,使用username去數據庫中查找到對應的權限,然后將權限賦值給這個用戶就可以實現權限的認證了