詳細(xì)請查看https://zhuanlan.zhihu.com/p/391839846
1. 概述
1.1 SpringBoot
這個就沒什么好說的了盼樟,能看到這個教程的爷抓,估計都是可以說精通了SpringBoot
的使用
1.2 Shiro
一個安全框架喉恋,但不只是一個安全框架处铛。它能實現(xiàn)多種多樣的功能失驶。并不只是局限在web層队询。在國內(nèi)的市場份額占比高于SpringSecurity
浙滤,是使用最多的安全框架
可以實現(xiàn)用戶的認(rèn)證和授權(quán)伶授。比SpringSecurity
要簡單的多沃呢。
1.3 Jwt
我的理解就是可以進(jìn)行客戶端與服務(wù)端之間驗證的一種技術(shù)年栓,取代了之前使用Session來驗證的不安全性
為什么不適用Session?
原理是薄霜,登錄之后客戶端和服務(wù)端各自保存一個相應(yīng)的SessionId某抓,每次客戶端發(fā)起請求的時候就得攜帶這個SessionId來進(jìn)行比對
- Session在用戶請求量大的時候服務(wù)器開銷太大了
- Session不利于搭建服務(wù)器的集群(也就是必須訪問原本的那個服務(wù)器才能獲取對應(yīng)的SessionId)
它使用的是一種令牌技術(shù)
Jwt字符串分為三部分
-
Header
存儲兩個變量
- 秘鑰(可以用來比對)
- 算法(也就是下面將Header和payload加密成Signature)
-
payload
存儲很多東西,基礎(chǔ)信息有如下幾個
- 簽發(fā)人惰瓜,也就是這個“令牌”歸屬于哪個用戶否副。一般是
userId
- 創(chuàng)建時間,也就是這個令牌是什么時候創(chuàng)建的
- 失效時間崎坊,也就是這個令牌什么時候失效
- 唯一標(biāo)識备禀,一般可以使用算法生成一個唯一標(biāo)識
- 簽發(fā)人惰瓜,也就是這個“令牌”歸屬于哪個用戶否副。一般是
-
Signature
這個是上面兩個經(jīng)過Header中的算法加密生成的,用于比對信息奈揍,防止篡改Header和payload
然后將這三個部分的信息經(jīng)過加密生成一個JwtToken
的字符串曲尸,發(fā)送給客戶端,客戶端保存在本地男翰。當(dāng)客戶端發(fā)起請求的時候攜帶這個到服務(wù)端(可以是在cookie
另患,可以是在header
,可以是在localStorage
中)蛾绎,在服務(wù)端進(jìn)行驗證
好了昆箕,廢話不多說了,下面開始實戰(zhàn)秘通,實戰(zhàn)分為以下幾個部分
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"
#認(rèn)證不通過的頁面
shiro.UnauthorizedUrl="xxx"
#授權(quán)不通過的跳轉(zhuǎn)頁面
創(chuàng)建ShiroConfig.java進(jìn)行一些簡單的配置
@Configuration
public class SpringShiroConfig {
@Bean
public Realm customRealm() {
return new CustomRealm();
}
@Bean
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(customRealm());
// 關(guān)閉 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"); // 權(quán)限不足錯誤提示接口
// 除了以上的請求外为严,其它請求都需要登錄
chain.addPathDefinition("/**", "authc");
return chain;
}
// Shiro 和 Spring AOP 整合時的特殊設(shè)置
@Bean
public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
creator.setProxyTargetClass(true);
return creator;
}
}
//還有關(guān)閉ShiroDao功能
創(chuàng)建自定義的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
<!-- 自動依賴導(dǎo)入 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();
// 強(qiáng)制指定注解的底層實現(xiàn)使用 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 代碼
-
創(chuàng)建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 簽發(fā)人名稱 * @param ttlMillis jwt 過期時間 * @param claims 額外添加到荷部分的信息肺稀。 * 例如可以添加用戶名、用戶ID应民、用戶(加密前的)密碼等信息 */ public String encode(String iss, long ttlMillis, Map<String, Object> claims) { if (claims == null) { claims = new HashMap<>(); } // 簽發(fā)時間(iat):荷載部分的標(biāo)準(zhǔn)字段之一 long nowMillis = System.currentTimeMillis(); Date now = new Date(nowMillis); // 下面就是在為payload添加各種標(biāo)準(zhǔn)聲明和私有聲明了 JwtBuilder builder = Jwts.builder() // 荷載部分的非標(biāo)準(zhǔn)字段/附加字段话原,一般寫在標(biāo)準(zhǔn)的字段之前夕吻。 .setClaims(claims) // JWT ID(jti):荷載部分的標(biāo)準(zhǔn)字段之一,JWT 的唯一性標(biāo)識繁仁,雖不強(qiáng)求涉馅,但盡量確保其唯一性。 .setId(UUID.randomUUID().toString()) // 簽發(fā)時間(iat):荷載部分的標(biāo)準(zhǔn)字段之一黄虱,代表這個 JWT 的生成時間稚矿。 .setIssuedAt(now) // 簽發(fā)人(iss):荷載部分的標(biāo)準(zhǔn)字段之一,代表這個 JWT 的所有者捻浦。通常是 username晤揣、userid 這樣具有用戶代表性的內(nèi)容。 .setSubject(iss) // 設(shè)置生成簽名的算法和秘鑰 .signWith(signatureAlgorithm, base64EncodedSecretKey); if (ttlMillis >= 0) { long expMillis = nowMillis + ttlMillis; Date exp = new Date(expMillis); // 過期時間(exp):荷載部分的標(biāo)準(zhǔn)字段之一朱灿,代表這個 JWT 的有效期昧识。 builder.setExpiration(exp); } return builder.compact(); } /** * JWT Token 由 頭部 荷載部 和 簽名部 三部分組成。簽名部分是由加密算法生成盗扒,無法反向解密跪楞。 * 而 頭部 和 荷載部分是由 Base64 編碼算法生成,是可以反向反編碼回原樣的侣灶。 * 這也是為什么不要在 JWT Token 中放敏感數(shù)據(jù)的原因甸祭。 * * @param jwtToken 加密后的token * @return claims 返回荷載部分的鍵值對 */ public Claims decode(String jwtToken) { // 得到 DefaultJwtParser return Jwts.parser() // 設(shè)置簽名的秘鑰 .setSigningKey(base64EncodedSecretKey) // 設(shè)置需要解析的 jwt .parseClaimsJws(jwtToken) .getBody(); } /** * 校驗 token * 在這里可以使用官方的校驗,或褥影, * 自定義校驗規(guī)則淋叶,例如在 token 中攜帶密碼,進(jìn)行加密處理后和數(shù)據(jù)庫中的加密密碼比較伪阶。 * * @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">在創(chuàng)建JwtUtil對象的時候需要傳入幾個數(shù)值</font>
- 這個用戶煞檩,用來生成秘鑰
- 這個加密算法,用來加密生成jwt
- 通過jwt數(shù)據(jù)獲取用戶信息的方法(decode())
- 判斷jwt是否存在或者過期的方法
- 最后是測試方法
- <font color="red">在創(chuàng)建JwtUtil對象的時候需要傳入幾個數(shù)值</font>
-
創(chuàng)建一個Controller
- 登錄的Controller
- 獲取username和password栅贴,進(jìn)行與數(shù)據(jù)庫的校驗斟湃,校驗成功執(zhí)行下一步,失敗直接返回
- 使用創(chuàng)建JwtUtil對象檐薯,傳入username和需要使用的加密算法
- 創(chuàng)建需要加在載荷中的一些基本信息的一個map對象
- 創(chuàng)建jwt數(shù)據(jù)凝赛,傳入username,保存時間坛缕,以及基本信息的map對象
- 校驗Controller
- 獲取前臺傳入的Jwt數(shù)據(jù)
- 使用
JWTUtil
中的isVerify
進(jìn)行該jwt數(shù)據(jù)有效的校驗
- 登錄的Controller
4. SpringBoot+Shiro+Jwt
-
由于需要對shiro的SecurityManager進(jìn)行設(shè)置墓猎,所以不能使用shiro-spring-boot-starter進(jìn)行與springboot的整合,只能使用spring-shiro
<!-- 自動依賴導(dǎo)入 shiro-core 和 shiro-web --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.4.1</version> </dependency>
-
由于需要實現(xiàn)無狀態(tài)的web赚楚,所以使用不到Shiro的Session功能毙沾,嚴(yán)謹(jǐn)點(diǎn)就是將其關(guān)閉
public class JwtDefaultSubjectFactory extends DefaultWebSubjectFactory { @Override public Subject createSubject(SubjectContext context) { // 不創(chuàng)建 session context.setSessionCreationEnabled(false); return super.createSubject(context); } }
這樣如果調(diào)用
getSession()
方法會拋出異常
4.1 流程
- 用戶請求,不攜帶token宠页,就在JwtFilter處拋出異常/返回沒有登錄左胞,讓它去登陸
- 用戶請求寇仓,攜帶token,就到JwtFilter中獲取jwt烤宙,封裝成JwtToken對象遍烦。然后使用JwtRealm進(jìn)行認(rèn)證
- 在JwtRealm中進(jìn)行認(rèn)證判斷這個token是否有效,也就是
執(zhí)行流程:1. 客戶端發(fā)起請求躺枕,shiro的過濾器生效服猪,判斷是否是login或logout的請求<br/> 如果是就直接執(zhí)行請求<br/> 如果不是就進(jìn)入JwtFilter2. JwtFilter執(zhí)行流程 1. 獲取header是否有"Authorization"的鍵,有就獲取拐云,沒有就拋出異常 2. 將獲取的jwt字符串封裝在創(chuàng)建的JwtToken中蔓姚,使用subject執(zhí)行l(wèi)ogin()方法進(jìn)行校驗。這個方法會調(diào)用創(chuàng)建的JwtRealm 3. 執(zhí)行JwtRealm中的認(rèn)證方法慨丐,使用`jwtUtil.isVerify(jwt)`判斷是否登錄過 4. 返回true就使基礎(chǔ)執(zhí)行下去
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) {
// 不創(chuàng)建 session
context.setSessionCreationEnabled(false);
return super.createSubject(context);
}
}
1. 創(chuàng)建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(),參數(shù)是=簽發(fā)人捅暴,存在時間恬砂,一些其他的信息=。返回值是JwtToken對應(yīng)的字符串
* 2. decode()蓬痒,參數(shù)是=JwtToken=泻骤。返回值是荷載部分的鍵值對
* 3. isVerify(),參數(shù)是=JwtToken=梧奢。返回值是這個JwtToken是否存在
* */
public class JwtUtil {
//創(chuàng)建默認(rèn)的秘鑰和算法狱掂,供無參的構(gòu)造方法使用
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;
}
/*
*這里就是產(chǎn)生jwt字符串的地方
* jwt字符串包括三個部分
* 1. header
* -當(dāng)前字符串的類型,一般都是“JWT”
* -哪種算法加密亲轨,“HS256”或者其他的加密算法
* 所以一般都是固定的趋惨,沒有什么變化
* 2. payload
* 一般有四個最常見的標(biāo)準(zhǔn)字段(下面有)
* iat:簽發(fā)時間,也就是這個jwt什么時候生成的
* jti:JWT的唯一標(biāo)識
* iss:簽發(fā)人惦蚊,一般都是username或者userId
* exp:過期時間
*
* */
public String encode(String iss, long ttlMillis, Map<String, Object> claims) {
//iss簽發(fā)人器虾,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的唯一標(biāo)識蹦锋,一般設(shè)置成唯一的兆沙,這個方法可以生成唯一標(biāo)識
.setIssuedAt(new Date(nowMillis))//1. 這個地方就是以毫秒為單位,換算當(dāng)前系統(tǒng)時間生成的iat
.setSubject(iss)//3. 簽發(fā)人莉掂,也就是JWT是給誰的(邏輯上一般都是username或者userId)
.signWith(signatureAlgorithm, base64EncodedSecretKey);//這個地方是生成jwt使用的算法和秘鑰
if (ttlMillis >= 0) {
long expMillis = nowMillis + ttlMillis;
Date exp = new Date(expMillis);//4. 過期時間葛圃,這個也是使用毫秒生成的,使用當(dāng)前時間+前面?zhèn)魅氲某掷m(xù)時間生成
builder.setExpiration(exp);
}
return builder.compact();
}
//相當(dāng)于encode的方向,傳入jwtToken生成對應(yīng)的username和password等字段装悲。Claim就是一個map
//也就是拿到荷載部分所有的鍵值對
public Claims decode(String jwtToken) {
// 得到 DefaultJwtParser
return Jwts.parser()
// 設(shè)置簽名的秘鑰
.setSigningKey(base64EncodedSecretKey)
// 設(shè)置需要解析的 jwt
.parseClaimsJws(jwtToken)
.getBody();
}
//判斷jwtToken是否合法
public boolean isVerify(String jwtToken) {
//這個是官方的校驗規(guī)則昏鹃,這里只寫了一個”校驗算法“尚氛,可以自己加
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); // 校驗不通過會拋出異常
//判斷合法的標(biāo)準(zhǔn):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. 創(chuàng)建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的時候進(jìn)行賬號密碼登錄,登錄成功允許訪問讯柔,登錄失敗拒絕訪問
* */
@Slf4j
public class JwtFilter extends AccessControlFilter {
/*
* 1. 返回true抡蛙,shiro就直接允許訪問url
* 2. 返回false,shiro才會根據(jù)onAccessDenied的方法的返回值決定是否允許訪問url
* */
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
log.warn("isAccessAllowed 方法被調(diào)用");
//這里先讓它始終返回false來使用onAccessDenied()方法
return false;
}
/**
* 返回結(jié)果為true表明登錄通過
*/
@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
log.warn("onAccessDenied 方法被調(diào)用");
//這個地方和前端約定魂迄,要求前端將jwtToken放在請求的Header部分
//所以以后發(fā)起請求的時候就需要在Header中放一個Authorization粗截,值就是對應(yīng)的Token
HttpServletRequest request = (HttpServletRequest) servletRequest;
String jwt = request.getHeader("Authorization");
log.info("請求的 Header 中藏有 jwtToken {}", jwt);
JwtToken jwtToken = new JwtToken(jwt);
/*
* 下面就是固定寫法
* */
try {
// 委托 realm 進(jìn)行登錄認(rèn)證
//所以這個地方最終還是調(diào)用JwtRealm進(jìn)行的認(rèn)證
getSubject(servletRequest, servletResponse).login(jwtToken);
//也就是subject.login(token)
} catch (Exception e) {
e.printStackTrace();
onLoginFail(servletResponse);
//調(diào)用下面的方法向客戶端返回錯誤信息
return false;
}
return true;
//執(zhí)行方法中沒有拋出異常就表示登錄成功
}
//登錄失敗時默認(rèn)返回 401 狀態(tài)碼
private void onLoginFail(ServletResponse response) throws IOException {
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
httpResponse.getWriter().write("login error");
}
}
3. 創(chuàng)建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
創(chuàng)建判斷jwt
是否有效的認(rèn)證方式的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
* 標(biāo)識這個Realm是專門用來驗證JwtToken
* 不負(fù)責(zé)驗證其他的token(UsernamePasswordToken)
* */
@Override
public boolean supports(AuthenticationToken token) {
//這個token就是從過濾器中傳入的jwtToken
return token instanceof JwtToken;
}
//授權(quán)
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
//認(rèn)證
//這個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");//判斷數(shù)據(jù)庫中username是否存在
log.info("在使用token登錄"+username);
return new SimpleAuthenticationInfo(jwt,jwt,"JwtRealm");
//這里返回的是類似賬號密碼的東西,但是jwtToken都是jwt字符串捣炬。還需要一個該Realm的類名
}
}
5. ShiroConfig
配置一些信息
- 因為不適用Session熊昌,所以為了防止會調(diào)用getSession()方法而產(chǎn)生錯誤,所以默認(rèn)調(diào)用自定義的Subject方法
- 一些修改湿酸,關(guān)閉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實現(xiàn)認(rèn)證有三個不一樣的地方婿屹,對應(yīng)下面abc
@Configuration
public class ShiroConfig {
/*
* a. 告訴shiro不要使用默認(rèn)的DefaultSubject創(chuàng)建對象,因為不能創(chuàng)建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
* */
// 關(guān)閉 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之外的請求都先經(jīng)過jwtFilter
* */
Map<String, Filter> filterMap = new HashMap<>();
//這個地方其實另外兩個filter可以不設(shè)置昂利,默認(rèn)就是
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 授權(quán)方面的信息
在JwtRealm中的授權(quán)部分,可以使用JwtUtil.decode(jwt).get("username")
獲取到username铁坎,使用username去數(shù)據(jù)庫中查找到對應(yīng)的權(quán)限蜂奸,然后將權(quán)限賦值給這個用戶就可以實現(xiàn)權(quán)限的認(rèn)證了