目錄???????
前言
為什么要登錄
登錄的種類(lèi)
Cookie-Session
Cookie-Session-local storage
JWT令牌
幾種登陸總結(jié)?
用戶(hù)身份認(rèn)證與授權(quán)
創(chuàng)建工程
添加依賴(lài)
啟動(dòng)項(xiàng)目
Bcrypt算法的工具
創(chuàng)建VO模型類(lèi)
創(chuàng)建接口文件
創(chuàng)建XML文件
補(bǔ)充配置
添加依賴(lài)
添加配置
創(chuàng)建配置類(lèi)
測(cè)試上面的配置
讓Spring Security通過(guò)數(shù)據(jù)庫(kù)驗(yàn)證密碼
配置密碼加密器
重寫(xiě)Spring Security下的用戶(hù)相關(guān)抽象方法
測(cè)試成果
JWT
什么是JWT
為什么使用JWT
如何使用JWT
添加依賴(lài)
測(cè)試jwt
在Spring Security中使用JWT
自動(dòng)裝配AuthenticationManager對(duì)象
創(chuàng)建DTO類(lèi)
創(chuàng)建接口類(lèi)
創(chuàng)建實(shí)現(xiàn)類(lèi)
創(chuàng)建控制器類(lèi)
測(cè)試代碼
返回客戶(hù)端JWT數(shù)據(jù)
修改AdminServiceImpl實(shí)現(xiàn)類(lèi)
修改控制器類(lèi)
測(cè)試jwt數(shù)據(jù)返回
使用其他URL被屏蔽怎么辦
使用請(qǐng)求頭
結(jié)語(yǔ)
前言
登錄這東西很奇怪哎,你說(shuō)它難嗎?好像客戶(hù)端只需要調(diào)接口就行,那有啥難的?當(dāng)你多多少少對(duì)登錄的后臺(tái)有些了解,又覺(jué)得好難啊,session,token,cookie,等等一堆東西,有老的大家都不喜歡用的,有新的一些不太懂的,根據(jù)公司項(xiàng)目規(guī)模不同,還要考慮成本的問(wèn)題,真是有些頭疼。博主今天推薦的一種登陸方式便是Spring Security + JWT的結(jié)合使用,為什么要兩者結(jié)合呢?Spring Security現(xiàn)在已經(jīng)很少用了,甚至有些人認(rèn)為已經(jīng)廢棄了,但是因?yàn)镾pring Security是Spring系列的東西,Spring對(duì)其支持很友好,不,是非常友好。但是我們不想使用他驗(yàn)證后的操作,所以我們要打斷這個(gè)操作,讓JWT工作。下面我們就來(lái)了解并手動(dòng)操作一下吧,本篇還是集成在我們前面的微服務(wù)項(xiàng)目中,你也可以另起項(xiàng)目一起來(lái)做。
為什么要登錄
我們平時(shí)都知道登錄,不知道有沒(méi)有思考過(guò)登錄解決的是什么問(wèn)題?大家會(huì)想到,不登錄就不能拿到用戶(hù)信息,一些用戶(hù)行為和服務(wù)就沒(méi)有辦法關(guān)聯(lián)到具體人身上,沒(méi)錯(cuò)!比如購(gòu)買(mǎi)行為。但我覺(jué)得這個(gè)說(shuō)法不夠具體,登錄的具體作用應(yīng)該是拿到用戶(hù)的權(quán)限。
我們說(shuō),是人,就有不同的角色,一個(gè)男人,可以是兒子,可以是父親,可以是員工,可以是老板等等。那我們就認(rèn)為,一個(gè)登錄體系中,必須要有一張用戶(hù)表和一張角色表,還有剛剛說(shuō)的權(quán)限表。這三張表之間還需要有表明其關(guān)系的關(guān)聯(lián)表。
可以說(shuō),這三張表在任何一個(gè)登錄體系都是必備的,甚至你還可以有臨時(shí)的用戶(hù)權(quán)限表,他們之間多是多對(duì)多的關(guān)系,要理清他們之間的關(guān)系并不簡(jiǎn)單。
登錄的種類(lèi)
登錄的種類(lèi)到目前為止所使用的技術(shù)大概五六種吧,其之間大同小異,從早期的Cookie-Session到現(xiàn)在的單點(diǎn)登錄,中間跨越的時(shí)間不短,其中有一個(gè)時(shí)間分割點(diǎn)就是html5標(biāo)準(zhǔn)出現(xiàn)的時(shí)候,他帶來(lái)了local storage,使得跨域問(wèn)題得到良好的解決,但我們并不滿(mǎn)足于這種方式,于是token技術(shù)出現(xiàn),但是本質(zhì)上和基于Cookie-Session+local storage的方式?jīng)]有太大區(qū)別。為了解決微服務(wù)間的數(shù)據(jù)同步,基于Token的JWT認(rèn)證誕生,其中還有一種利用session和redis的數(shù)據(jù)共享技術(shù)也能實(shí)現(xiàn)數(shù)據(jù)共享,這和token技術(shù)也類(lèi)似。接下來(lái),我們來(lái)簡(jiǎn)單的了解一下這幾種登錄方式:
Cookie-Session
這種方式要追溯到html5出現(xiàn)之前,那時(shí)只能利用cookie存儲(chǔ)SessionId,但cookie在跨域問(wèn)題上一言難盡,但并不是不能跨域,只是要比我們后面的方法麻煩,有l(wèi)ocal storage你還用cookie?而且cookie退出站點(diǎn)后就會(huì)銷(xiāo)毀,這點(diǎn)讓人極不能接受。其流程是:
- 用戶(hù)輸入用戶(hù)名、密碼或短信驗(yàn)證碼登錄
- 服務(wù)端驗(yàn)證后,返回一個(gè) SessionId,其和用戶(hù)信息關(guān)聯(lián)??蛻?hù)端將 SessionID存到cookie
- 當(dāng)客戶(hù)端再發(fā)起請(qǐng)求時(shí)帶上cookie中的信息,服務(wù)端通過(guò)cookie獲取 SessionID并校驗(yàn),以判斷用戶(hù)是否登錄
?
Cookie-Session-local storage
這種方式和以上相似,只是改了幾個(gè)地方:
- 存儲(chǔ)的位置不再是cookie,而是local storage
- 服務(wù)端不存儲(chǔ)sessionid,而是改用redis做存儲(chǔ),可以解決同步問(wèn)題,但也有缺點(diǎn),同步會(huì)造成數(shù)據(jù)量增加,占用額外內(nèi)存,我們通過(guò)一張圖來(lái)說(shuō)明
左邊先行,獲取用戶(hù)信息,生成sessionid,存儲(chǔ)在redis,右邊訪(fǎng)問(wèn)其他模塊,通過(guò)sessionid去redis拿用戶(hù)信息,注意,用戶(hù)模塊和其他模塊也會(huì)保存sessionid,這就是數(shù)據(jù)共享,用戶(hù)量很大的情況會(huì)造成數(shù)據(jù)冗余,不適合用戶(hù)量特別大的項(xiàng)目,中小型項(xiàng)目可以。對(duì)于客戶(hù)端,sessionid當(dāng)然是保存在local storage內(nèi)了,畢竟誰(shuí)也不想去額外解決跨域的問(wèn)題。
JWT令牌
這種方式是目前使用比較多的一種方式,它和上面的方式也有相似之處,只是少了數(shù)據(jù)的存儲(chǔ),JWT不存儲(chǔ)session這些東西,它只負(fù)責(zé)驗(yàn)證jwt是否正確,驗(yàn)證的過(guò)程就是解碼的過(guò)程,關(guān)于JWT的標(biāo)準(zhǔn)制式的解釋?zhuān)?qǐng)大家手動(dòng)百度吧,不再贅述,博主也記不住,貼了浪費(fèi)篇幅??纯?,大概知道是怎么做的就行。
此處必須有圖:
服務(wù)端不保存信息,這一點(diǎn)可以節(jié)省空間,誰(shuí)的信息誰(shuí)自己保存,解密方式在我這里,同時(shí)提高了安全性,何樂(lè)不為?
幾種登陸總結(jié)?
如果細(xì)分還能再分出幾種登錄方式,但基本大同小異,博主合并了其中相似的登錄方式,總結(jié)出來(lái)這三種,此處忽略第三方登錄,可自行了解??隙ㄟ€有其他方式,但總的來(lái)說(shuō),和這三種應(yīng)該是類(lèi)似,并不會(huì)完全不同??戳艘黄狾Auth2.0單點(diǎn)登錄相關(guān)的文章,還有一篇總結(jié)登錄的文章,真是寫(xiě)的太好了,分享給大家:
安全驗(yàn)證 - 知乎
Java——項(xiàng)目常用登錄方式詳解_new 海綿寶寶()的博客-CSDN博客
里面總結(jié)的很全面,也有一些案例,初學(xué)者可以看看。
用戶(hù)身份認(rèn)證與授權(quán)
從這里開(kāi)始,就是我們的項(xiàng)目時(shí)間,首先出場(chǎng)的是Spring Security,它是用于解決認(rèn)證與授權(quán)的框架。Spring Security有默認(rèn)的登錄賬號(hào)和密碼,用戶(hù)名user,密碼是隨機(jī)的,每次啟動(dòng)項(xiàng)目都會(huì)重新生成一個(gè)。它要求所有的請(qǐng)求都必須先登錄才允許訪(fǎng)問(wèn),稍后我們集成后可以來(lái)進(jìn)行測(cè)試。
創(chuàng)建工程
在微服務(wù)項(xiàng)目cloud下創(chuàng)建cloud-passport子項(xiàng)目:
添加依賴(lài)
<?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>com.codingfire</groupId> <artifactId>cloud</artifactId> <version>0.0.1-SNAPSHOT</version> </parent> <groupId>com.codingfire</groupId> <artifactId>cloud-passport</artifactId> <version>0.0.1-SNAPSHOT</version> <name>cloud-passport</name> <description>Demo project for Spring Boot</description> <dependencies> <!-- Spring Boot Web:支持Spring MVC --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Spring Boot Security:處理認(rèn)證與授權(quán) --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- Spring Boot Test:測(cè)試 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> </project>
父子關(guān)聯(lián)
<modules> <module>cloud-commons</module> <module>cloud-bussiness</module> <module>cloud-cart</module> <module>cloud-order</module> <module>cloud-stock</module> <module>gateway</module> <module>search</module> <module>cloud-passport</module> </modules>
啟動(dòng)項(xiàng)目
依賴(lài)添加完畢,什么都不需要做,直接運(yùn)行passport的啟動(dòng)文件,可以在控制臺(tái)看到如下輸出:
Using generated security password: 1060ee9f-a56e-4ff5-bce4-68306b3265b1
這就是Spring Security生成的隨機(jī)密碼,它同時(shí)還提供了一個(gè)URL:http://localhost:8080/login?
我們點(diǎn)開(kāi)URL,在瀏覽器打開(kāi)一個(gè)登錄頁(yè)面,我們輸入用戶(hù)名:user,密碼就用上面的密碼,登錄成功后跳轉(zhuǎn)回之前訪(fǎng)問(wèn)的URL,由于我們沒(méi)有做這個(gè)頁(yè)面,會(huì)顯示404。這就是Spring Security默認(rèn)要求所有的請(qǐng)求都是必須先登錄才允許的訪(fǎng)問(wèn)的能力。
Bcrypt算法的工具
Spring Security的依賴(lài)項(xiàng)中包括了Bcrypt算法的工具類(lèi),這是一款非常優(yōu)秀的密碼加密工具,適和對(duì)需要存儲(chǔ)下來(lái)的密碼進(jìn)行加密處理。我們來(lái)測(cè)試下看看。
打開(kāi)測(cè)試類(lèi),添加如下測(cè)試代碼:
package com.codingfire.cloud.passport;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@SpringBootTest
class CloudPassportApplicationTests {
private BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
@Test
public void testEncode() {
// 原文相同的情況,每次加密得到的密文都不同
for (int i = 0; i < 10; i++) {
String rawPassword = "123456";
String encodedPassword = passwordEncoder.encode(rawPassword);
System.out.println("rawPassword = " + rawPassword);
System.out.println("encodedPassword = " + encodedPassword);
}
}
@Test
public void testMatches() {
String rawPassword = "123456";
String encodedPassword = "$2a$10$4LHozWwptKuabvikrzM1KefYFgI7H4A9xCVv7cvMKsV9ycS4guS5K";
boolean matchResult = passwordEncoder.matches(rawPassword, encodedPassword);
System.out.println("match result : " + matchResult);
}
}
我們分別運(yùn)行這兩個(gè)方法,會(huì)看到如下輸出:
rawPassword = 123456 encodedPassword = $2a$10$4LHozWwptKuabvikrzM1KefYFgI7H4A9xCVv7cvMKsV9ycS4guS5K rawPassword = 123456 encodedPassword = $2a$10$VA9u7X9rSvuEtPlEixhnSujHdVsK8OwqkVIOqLzNydxa.ypCviVIq rawPassword = 123456 encodedPassword = $2a$10$d9lWItH5YhEFRns/Yj5U3OUyHM8rLKAE9X.SsbcIOA0WwRqUwFl82 rawPassword = 123456 encodedPassword = $2a$10$W/PLc/Q04.8xfmEQgwSKC.g79FxRPJGFXRuFzISdVrn3cYWk1xkye rawPassword = 123456 encodedPassword = $2a$10$/9Ya1aqjQBX8342iH5blTOZeHJomKUitInVmLTsANonXriQjxhb5K rawPassword = 123456 encodedPassword = $2a$10$kX2u5zLrDN/VC8CLVRGmsOIFqA2FHCJRYJKnYmWeu/NyTQEjBCbki rawPassword = 123456 encodedPassword = $2a$10$igB96QfY9XDwhPz3U8Z7Nui1UQy.wtzSl9uk2n7m.lCdcKwhGqLXu rawPassword = 123456 encodedPassword = $2a$10$ssDypFmm0bN0CvIBqoB4huHIhT7oRwS9KsO1iopyFeSOUWYR96NPC rawPassword = 123456 encodedPassword = $2a$10$IWBuDVLYjvHCUqOM9qAQuu.kTlW8RH08CbIFlvYTzcdEMLHbVSFtS rawPassword = 123456 encodedPassword = $2a$10$J/eN5/loO6DTJG7ubgQh4.1ovwI9CS1H0yqnsbYEQFwnvqRq64bU.
match result : true
下面的解密使用上面的第一個(gè)加密后的密文進(jìn)行的解密。大家要用自己的電腦生成的密文進(jìn)行解密,用博主的可能會(huì)出現(xiàn)無(wú)法匹配的情況。
此加密工具有個(gè)特點(diǎn),大家應(yīng)該發(fā)現(xiàn)了,此加密得到的密文都不相同。
接著需要和數(shù)據(jù)庫(kù)中存儲(chǔ)的密文進(jìn)行對(duì)比,此時(shí)需要使用SQL去數(shù)據(jù)庫(kù)查詢(xún)?cè)撚脩?hù)的密文進(jìn)行比對(duì),比對(duì)通過(guò),則可進(jìn)行登錄。此時(shí)就不能使用默認(rèn)的user
用戶(hù)名和隨機(jī)的密碼的方式,具體做法我們繼續(xù)往下看。
創(chuàng)建VO模型類(lèi)
在commons工程下創(chuàng)建pojo.passport.vo.AdminLoginVO類(lèi):
package com.codingfire.cloud.commons.pojo.passport.vo;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
@Data
public class AdminLoginVO implements Serializable {
private Long id;
private String username;
private String password;
private Integer isLogin;
private List<String> permissions;
}
?創(chuàng)建完成后我們發(fā)現(xiàn)要使用commons模塊,那需要依賴(lài)添加此模塊:
<!--all-common依賴(lài)--> <dependency> <groupId>com.codingfire</groupId> <artifactId>cloud-commons</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency>
創(chuàng)建接口文件
在passport下創(chuàng)建mapper.AdminMapper
接口:
package com.codingfire.cloud.passport.mapper;
import com.codingfire.cloud.commons.pojo.passport.vo.AdminLoginVO;
public interface AdminMapper {
AdminLoginVO getLoginInfoByUsername(String username);
}
創(chuàng)建XML文件
大家還記得嗎?我們?cè)贛ybatis框架中有使用XML文件來(lái)寫(xiě)SQL。在src/main/resources下創(chuàng)建mapper文件夾,mapper文件夾下可以把前面的xml文件粘貼過(guò)來(lái),寫(xiě)入如下SQL:
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.codingfire.cloud.passport.mapper.AdminMapper"> <!-- AdminLoginVO getLoginInfoByUsername(String username); --> <select id="getLoginInfoByUsername" resultMap="LoginInfoResultMap"> select <include refid="LoginInfoQueryFields" /> from admin left join admin_role on admin.id = admin_role.admin_id left join role_permission on admin_role.role_id = role_permission.role_id left join permission on role_permission.permission_id = permission.id where username=#{username} </select> <sql id="LoginInfoQueryFields"> <if test="true"> admin.id, admin.username, admin.password, admin.is_login, permission.name </if> </sql> <resultMap id="LoginInfoResultMap" type="com.codingfire.cloud.commons.pojo.passport.vo.AdminLoginVO"> <id column="id" property="id" /> <result column="username" property="username" /> <result column="password" property="password" /> <result column="is_login" property="isLogin" /> <collection property="permissions" ofType="java.lang.String"> <constructor> <arg column="name" /> </constructor> </collection> </resultMap> </mapper>
在這里大家要留意幾個(gè)問(wèn)題了,我們這里需要連接mybatis的數(shù)據(jù)庫(kù),第一次看博主文章的需要看看Java開(kāi)發(fā) - Mybatis框架初體驗(yàn)_CodingFire的博客-CSDN博客
這篇博客,才知道建的什么數(shù)據(jù)庫(kù), 有哪些表,有哪些參數(shù),否則將很難進(jìn)行下去。
補(bǔ)充配置
由于需要使用數(shù)據(jù)庫(kù),需要補(bǔ)充配置和依賴(lài)。
添加依賴(lài)
<!--mybatis整合springboot--> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> </dependency> <!--alibaba 數(shù)據(jù)源德魯伊--> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> </dependency> <!--mysql驅(qū)動(dòng)--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency>
添加配置
這里,我們選擇從mybatis復(fù)制配置信息到properties文件:
spring.datasource.url=jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true spring.datasource.driver=com.mysql.cj.jdbc.Driver spring.datasource.username=root spring.datasource.password=0 mybatis.mapper-locations=classpath:mapper/AdminMapper.xml
密碼寫(xiě)自己的數(shù)據(jù)庫(kù)密碼。
創(chuàng)建配置類(lèi)
需要連接數(shù)據(jù)庫(kù),那么少不了mybatis配置了,創(chuàng)建MybatisConfiguration類(lèi),在passport下創(chuàng)建config包,此包下創(chuàng)建配置類(lèi):
package com.codingfire.cloud.passport.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan("com.codingfire.cloud.passport.mapper")
public class MybatisConfiguration {
}
前面也是有創(chuàng)建過(guò)的,你可以選擇直接貼過(guò)來(lái),但要注意掃描的路徑改成自己的路徑。原本需要在配置文件中配置mybatis.mapper-locations
屬性,上面已經(jīng)補(bǔ)充過(guò)了。
測(cè)試上面的配置
在測(cè)試類(lèi)下,我們添加如下代碼:
@Autowired
AdminMapper adminMapper;
@Test
void selectUser() {
AdminLoginVO adminLoginVO = adminMapper.getLoginInfoByUsername("admin04");
System.out.println(adminLoginVO);
}
這是我們?cè)瓉?lái)表中的數(shù)據(jù),沒(méi)有數(shù)據(jù)的需要預(yù)先插入一些數(shù)據(jù)。運(yùn)行測(cè)試方法,發(fā)現(xiàn)報(bào)錯(cuò)?
額,一大堆,看了......好一會(huì)兒,才發(fā)現(xiàn)有兩個(gè)地方寫(xiě)錯(cuò)了:
一個(gè)是AdminMapper內(nèi)沒(méi)有添加@Repository注解:
另一個(gè)是MybatisConfiguration類(lèi)上的scan注解寫(xiě)錯(cuò)了,修改一下:
然后再次運(yùn)行測(cè)試方法,可以在控制臺(tái)看到輸出的用戶(hù)信息如下:
AdminLoginVO(id=1, username=admin04, password=123456, isLogin=0, permissions=[全頻道可刪除, 全頻道可篩選, 全頻道讀取, 單頻道可刪除, 單頻道可篩選, 單頻道觀看])?
?代表我們的測(cè)試成功了。簡(jiǎn)直累的一逼,真是錯(cuò)一步都不行。
讓Spring Security通過(guò)數(shù)據(jù)庫(kù)驗(yàn)證密碼
前面提過(guò),要讓Spring Security通過(guò)數(shù)據(jù)庫(kù)的數(shù)據(jù)來(lái)驗(yàn)證用戶(hù)名與密碼,我們還需要做出一些修改和配置,我們看到每次控制臺(tái)都會(huì)輸出一串新的密碼:
Using generated security password: a47b9983-3ea3-45d8-9632-faf701a7925b
下面,讓我們看看怎樣才能不讓它輸出。
配置密碼加密器
在config包下創(chuàng)建SecurityConfiguration類(lèi):
package com.codingfire.cloud.passport.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class SecurityConfiguration {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
重寫(xiě)Spring Security下的用戶(hù)相關(guān)抽象方法
在passport下建新包security,包下建類(lèi)UserDetailsServiceImpl,并實(shí)現(xiàn)UserDetailsService接口:
package com.codingfire.cloud.passport.security;
import com.codingfire.cloud.commons.pojo.passport.vo.AdminLoginVO;
import com.codingfire.cloud.passport.mapper.AdminMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private AdminMapper adminMapper;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
System.out.println("根據(jù)用戶(hù)名查詢(xún)嘗試登錄的管理員信息,用戶(hù)名=" + s);
AdminLoginVO admin = adminMapper.getLoginInfoByUsername(s);
System.out.println("通過(guò)持久層進(jìn)行查詢(xún),結(jié)果=" + admin);
if (admin == null) {
System.out.println("根據(jù)用戶(hù)名沒(méi)有查詢(xún)到有效的管理員數(shù)據(jù),將拋出異常");
throw new BadCredentialsException("登錄失敗,用戶(hù)名不存在!");
}
System.out.println("查詢(xún)到匹配的管理員數(shù)據(jù),需要將此數(shù)據(jù)轉(zhuǎn)換為UserDetails并返回");
UserDetails userDetails = User.builder()
.username(admin.getUsername())
.password(admin.getPassword())
.accountExpired(false)
.accountLocked(false)
.disabled(admin.getIsLogin() != 1)
.credentialsExpired(false)
.authorities(admin.getPermissions().toArray(new String[] {}))
.build();
System.out.println("轉(zhuǎn)換得到UserDetails=" + userDetails);
return userDetails;
}
}
測(cè)試成果
重新啟動(dòng)工程,看看還有沒(méi)有隨機(jī)密碼生成:
可以看到,隨機(jī)密碼已經(jīng)不會(huì)再自動(dòng)生成。
JWT
什么是JWT
Json web token (JWT), 是為了在網(wǎng)絡(luò)應(yīng)用環(huán)境間傳遞聲明而執(zhí)行的一種基于JSON
的開(kāi)放標(biāo)準(zhǔn)((RFC 7519).定義了一種簡(jiǎn)潔的,自包含的方法用于通信雙方之間以JSON
對(duì)象的形式安全的傳遞信息。因?yàn)閿?shù)字簽名的存在,這些信息是可信的,JWT可以使用HMAC
算法或者是RSA
的公私秘鑰對(duì)進(jìn)行簽名。
客戶(hù)端第1次訪(fǎng)問(wèn)服務(wù)器端時(shí),是沒(méi)有攜帶令牌訪(fǎng)問(wèn)的,當(dāng)服務(wù)器進(jìn)行響應(yīng)時(shí),會(huì)將JWT響應(yīng)到客戶(hù)端,客戶(hù)端保存后,在第2次訪(fǎng)問(wèn)時(shí)就開(kāi)始攜帶JWT進(jìn)行請(qǐng)求,服務(wù)器收到請(qǐng)求中的JWT后就可以識(shí)別用戶(hù)身份。
關(guān)于JWT的詳細(xì)介紹,推薦這篇博客:SpringBoot集成JWT實(shí)現(xiàn)token驗(yàn)證 - 簡(jiǎn)書(shū)
為什么使用JWT
Spring Security默認(rèn)使用Session機(jī)制存儲(chǔ)用戶(hù)信息,而HTTP協(xié)議是無(wú)狀態(tài)協(xié)議,它不保存客戶(hù)端信息,所以,同一個(gè)客戶(hù)端的多次訪(fǎng)問(wèn),等效于多個(gè)不同的客戶(hù)端各訪(fǎng)問(wèn)一次服務(wù)端,為了保存用戶(hù)信息,使服務(wù)器端能夠識(shí)別客戶(hù)端身份,我們推薦使用Token或其他技術(shù),比如我們馬上要說(shuō)的JWT。
如何使用JWT
添加依賴(lài)
JWT只是一個(gè)概念,而實(shí)現(xiàn)生成JWT、解析JWT的框架卻有不少,我們這里要使用的是jjwt,添加依賴(lài)如下:
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt --> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> </dependency>
由于版本已經(jīng)在主項(xiàng)目中控制,此處版本省略。
測(cè)試jwt
在測(cè)試類(lèi)下創(chuàng)建JwtTests類(lèi),添加如下測(cè)試代碼:
// 密鑰,遵從越長(zhǎng)越好,越亂越復(fù)雜越好的原則
String secretKey = "asjdkahwehuqdyaoisdqwuphdabskbkansdjashdjasdh";
@Test
public void testGenerateJwt() {
// Claims
Map<String, Object> claims = new HashMap<>();
claims.put("id", 01);
claims.put("name", "codingfire");
// JWT的組成部分:Header(頭),Payload(載荷),Signature(簽名)
String jwt = Jwts.builder()
// Header:指定算法與當(dāng)前數(shù)據(jù)類(lèi)型
// 格式為: { "alg": 算法, "typ": "jwt" }
.setHeaderParam(Header.CONTENT_TYPE, "HS256")
.setHeaderParam(Header.TYPE, Header.JWT_TYPE)
// Payload:通常包含Claims(自定義數(shù)據(jù))和過(guò)期時(shí)間
.setClaims(claims)
.setExpiration(new Date(System.currentTimeMillis() + 5 * 60 * 1000))
// Signature:由算法和密鑰(secret key)這2部分組成
.signWith(SignatureAlgorithm.HS256, secretKey)
// 打包生成
.compact();
System.out.println(jwt);
}
運(yùn)行測(cè)試方法,輸出加密后的密文如下:
eyJjdHkiOiJIUzI1NiIsInR5cCI6IkpXVCIsImFsZyI6IkhTMjU2In0.eyJuYW1lIjoiY29kaW5nZmlyZSIsImlkIjoxLCJleHAiOjE2Nzc3MzgyNzZ9.cz_cjbIT28GgZ5gQFkgOEAVmqjqFRW2MIliGftfT2As
你能看到里面有兩個(gè)點(diǎn),這是JWT加密的固定格式,需要你去看推薦的博文。
接著我們把這串密文用來(lái)解密試試看能得到什么:
@Test
public void testParseJwt() {
String jwt = "eyJjdHkiOiJIUzI1NiIsInR5cCI6IkpXVCIsImFsZyI6IkhTMjU2In0.eyJuYW1lIjoiY29kaW5nZmlyZSIsImlkIjoxLCJleHAiOjE2Nzc3MzgyNzZ9.cz_cjbIT28GgZ5gQFkgOEAVmqjqFRW2MIliGftfT2As";
Claims claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt).getBody();
Object id = claims.get("id");
Object name = claims.get("name");
System.out.println("id=" + id);
System.out.println("name=" + name);
}
運(yùn)行測(cè)試方法:
看到如圖所示結(jié)果,你的jwt就已經(jīng)引入成功。但,這還不夠,我們是要在Spring Security中使用JWT,所以還有很多工作要做。
在Spring Security中使用JWT
自動(dòng)裝配AuthenticationManager對(duì)象
這是一個(gè)認(rèn)證管理器,我們需要接管這個(gè)管理器,在SecurityConfiguration類(lèi)中做一些操作,來(lái)看看最終的SecurityConfiguration類(lèi)吧:
package com.codingfire.cloud.passport.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 禁用防跨域攻擊
http.csrf().disable();
// URL白名單
String[] urls = {
"/admins/login"
};
// 配置各請(qǐng)求路徑的認(rèn)證與授權(quán)
http.authorizeRequests() // 請(qǐng)求需要授權(quán)才可以訪(fǎng)問(wèn)
.antMatchers(urls) // 匹配一些路徑
.permitAll() // 允許直接訪(fǎng)問(wèn)(不需要經(jīng)過(guò)認(rèn)證和授權(quán))
.anyRequest() // 匹配除了以上配置的其它請(qǐng)求
.authenticated(); // 都需要認(rèn)證
}
}
創(chuàng)建DTO類(lèi)
在上面創(chuàng)建AdminLoginVO類(lèi)的地方創(chuàng)建新的包dto,下面建新類(lèi):
創(chuàng)建接口類(lèi)
在passport下創(chuàng)建service包,其下創(chuàng)建新接口類(lèi)IAdminService:
package com.codingfire.cloud.passport.service;
import com.codingfire.cloud.commons.pojo.passport.dto.AdminLoginDTO;
public interface IAdminService {
String login(AdminLoginDTO adminLoginDTO);
}
創(chuàng)建實(shí)現(xiàn)類(lèi)
在service包下創(chuàng)建新包impl,其下創(chuàng)建實(shí)現(xiàn)類(lèi)AdminServiceImpl:
package com.codingfire.cloud.passport.service.impl;
import com.codingfire.cloud.commons.pojo.passport.dto.AdminLoginDTO;
import com.codingfire.cloud.passport.service.IAdminService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service;
@Service
public class AdminServiceImpl implements IAdminService {
@Autowired
private AuthenticationManager authenticationManager;
@Override
public String login(AdminLoginDTO adminLoginDTO) {
// 生成此用戶(hù)數(shù)據(jù)的JWT
String jwt = "This is a JWT."; // 臨時(shí)
return jwt;
}
}
創(chuàng)建控制器類(lèi)
在passport下創(chuàng)建controller包,其下創(chuàng)建AdminController:
package com.codingfire.cloud.passport.controller;
import com.codingfire.cloud.commons.pojo.passport.dto.AdminLoginDTO;
import com.codingfire.cloud.passport.service.IAdminService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping(value = "/admins", produces = "application/json; charset=utf-8")
public class AdminController {
@Autowired
private IAdminService adminService;
@RequestMapping("/login")
public String login(AdminLoginDTO adminLoginDTO) {
String jwt = adminService.login(adminLoginDTO);
return jwt;
}
}
測(cè)試代碼
啟動(dòng)項(xiàng)目,在瀏覽器輸入:http://localhost:8080/admins/login?username= codingfire&password=123456??
把用戶(hù)名和密碼改成你自己數(shù)據(jù)庫(kù)中的用戶(hù)名和密碼,也可以寫(xiě)錯(cuò)的,然后進(jìn)行多次嘗試,看瀏覽器會(huì)返回什么:
?看到此信息,就代表你的jwt接入成功了,但我們需要返回給客戶(hù)端jwt數(shù)據(jù),接下來(lái)我們實(shí)現(xiàn)這個(gè)過(guò)程。
返回客戶(hù)端JWT數(shù)據(jù)
修改AdminServiceImpl實(shí)現(xiàn)類(lèi)
package com.codingfire.cloud.passport.service.impl;
import com.codingfire.cloud.commons.pojo.passport.dto.AdminLoginDTO;
import com.codingfire.cloud.passport.service.IAdminService;
import io.jsonwebtoken.Header;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.User;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Service
public class AdminServiceImpl implements IAdminService {
@Autowired
private AuthenticationManager authenticationManager;
@Override
public String login(AdminLoginDTO adminLoginDTO) {
// 密鑰,遵從越長(zhǎng)越好,越亂越復(fù)雜越好的原則
String secretKey = "asjdkahwehuqdyaoisdqwuphdabskbkansdjashdjasdh";
// 準(zhǔn)備被認(rèn)證數(shù)據(jù)
Authentication authentication
= new UsernamePasswordAuthenticationToken(
adminLoginDTO.getUsername(), adminLoginDTO.getPassword());
// 調(diào)用AuthenticationManager驗(yàn)證用戶(hù)名與密碼
// 執(zhí)行認(rèn)證,如果此過(guò)程沒(méi)有拋出異常,則表示認(rèn)證通過(guò),如果認(rèn)證信息有誤,將拋出異常
authenticationManager.authenticate(authentication);
User user = (User) authentication.getPrincipal();
System.out.println("從認(rèn)證結(jié)果中獲取Principal=" + user.getClass().getName());
Map<String, Object> claims = new HashMap<>();
claims.put("username", user.getUsername());
claims.put("permissions", user.getAuthorities());
System.out.println("即將向JWT中寫(xiě)入數(shù)據(jù)=" + claims);
// JWT的組成部分:Header(頭),Payload(載荷),Signature(簽名)
String jwt = Jwts.builder()
// Header:指定算法與當(dāng)前數(shù)據(jù)類(lèi)型
// 格式為: { "alg": 算法, "typ": "jwt" }
.setHeaderParam(Header.CONTENT_TYPE, "HS256")
.setHeaderParam(Header.TYPE, Header.JWT_TYPE)
// Payload:通常包含Claims(自定義數(shù)據(jù))和過(guò)期時(shí)間
.setClaims(claims)
.setExpiration(new Date(System.currentTimeMillis() + 5 * 60 * 1000))
// Signature:由算法和密鑰(secret key)這2部分組成
.signWith(SignatureAlgorithm.HS256, secretKey)
// 打包生成
.compact();
// 返回JWT數(shù)據(jù)
return jwt;
}
}
?你會(huì)發(fā)現(xiàn),這就是我們?cè)跍y(cè)試類(lèi)中測(cè)試的代碼,基本上是直接貼過(guò)來(lái)的。
修改控制器類(lèi)
package com.codingfire.cloud.passport.controller;
import com.codingfire.cloud.commons.pojo.passport.dto.AdminLoginDTO;
import com.codingfire.cloud.commons.restful.JsonResult;
import com.codingfire.cloud.passport.service.IAdminService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping(value = "/admins", produces = "application/json; charset=utf-8")
public class AdminController {
@Autowired
private IAdminService adminService;
@RequestMapping("/login")
public JsonResult<String> login(AdminLoginDTO adminLoginDTO) {
String jwt = adminService.login(adminLoginDTO);
return JsonResult.ok(jwt);
}
}
修改返回值類(lèi)型。
測(cè)試jwt數(shù)據(jù)返回
運(yùn)行項(xiàng)目,在瀏覽器輸入原來(lái)的html:?http://localhost:8080/admins/login?username= codingfire&password=123456?
瀏覽器將得到如下數(shù)據(jù):
{"state":200,"message":"eyJjdHkiOiJIUzI1NiIsInR5cCI6IkpXVCIsImFsZyI6IkhTMjU2In0.eyJwZXJtaXNzaW9ucyI6Ilt7XCJhdXRob3JpdHlcIjpcIuWFqOmikemBk-WPr-WIoOmZpFwifSx7XCJhdXRob3JpdHlcIjpcIuWFqOmikemBk-WPr-etm-mAiVwifSx7XCJhdXRob3JpdHlcIjpcIuWFqOmikemBk-ivu-WPllwifSx7XCJhdXRob3JpdHlcIjpcIuWNlemikemBk-WPr-WIoOmZpFwifSx7XCJhdXRob3JpdHlcIjpcIuWNlemikemBk-WPr-etm-mAiVwifSx7XCJhdXRob3JpdHlcIjpcIuWNlemikemBk-ingueci1wifV0iLCJleHAiOjE2Nzc3NDkzOTUsInVzZXJuYW1lIjoiY29kaW5nZmlyZSJ9.dw4tk52xTXXQ4-D_qkZNhjL-RkHnzG6QKHe6Tq1j3_Y","data":null}
這里有個(gè)坑啊小伙伴們,如果你一直403,且控制臺(tái)提示你Encoded password does not look like BCrypt,這是因?yàn)槟愕臄?shù)據(jù)庫(kù)存儲(chǔ)的是明文密碼,必須存儲(chǔ)我們?cè)跍y(cè)試類(lèi)中使用BCryptPasswordEncoder加密后的密碼。博主剛剛就犯了這個(gè)錯(cuò),真實(shí)太容易忽略了,不知道該說(shuō)啥,大家可別犯這個(gè)錯(cuò)。
使用其他URL被屏蔽怎么辦
剛剛由于我們禁止了未登陸時(shí)直接進(jìn)入Spring Security的登陸頁(yè),所以才需要添加了白名單解決屏蔽所有連接的問(wèn)題。如果使用Knife4j,該怎么添加白名單呢?我們來(lái)看看:
String[] urls = {
"/admins/login",
"/doc.html", // 從本行開(kāi)始,以下是新增
"/**/*.js",
"/**/*.css",
"/swagger-resources",
"/v2/api-docs",
"/favicon.ico"
};
使用請(qǐng)求頭
得到JWT之后,在后續(xù)的請(qǐng)求中都需要在請(qǐng)求頭中帶上JWT,放在Authorization屬性?xún)?nèi),所以應(yīng)該先判斷請(qǐng)求頭中是否有Authorization,而不能讓請(qǐng)求直達(dá)服務(wù)器業(yè)務(wù)模塊。這讓我想到了前面講過(guò)的過(guò)濾器,下面,我們?cè)趕ecurity包下創(chuàng)建一個(gè)過(guò)濾器類(lèi):
package com.codingfire.cloud.passport.security;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
System.out.println("JwtAuthenticationFilter.doFilterInternal()");
}
}
過(guò)濾器類(lèi)是需要注冊(cè)后才能工作的,所以下一步對(duì)過(guò)濾器進(jìn)行注冊(cè)。用于驗(yàn)證JWT的過(guò)濾器應(yīng)該運(yùn)行在Spring Security處理登錄的過(guò)濾器之前才能工作,所以需要在自定義的SecurityConfiguration
中的configure()
方法中將我們自定義的過(guò)濾器注冊(cè)在Spring Security的相關(guān)過(guò)濾器之前。
同一個(gè)項(xiàng)目中允許存在多個(gè)過(guò)濾器,形成過(guò)濾器鏈,所以我們注冊(cè)過(guò)濾器不需要單獨(dú)建個(gè)類(lèi)來(lái)處理了,而是在SecurityConfiguration類(lèi)中進(jìn)行,最終的類(lèi)如下:
package com.codingfire.cloud.passport.config;
import com.codingfire.cloud.passport.security.JwtAuthenticationFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 禁用防跨域攻擊
http.csrf().disable();
// URL白名單
String[] urls = {
"/admins/login",
"/doc.html", // 從本行開(kāi)始,以下是新增
"/**/*.js",
"/**/*.css",
"/swagger-resources",
"/v2/api-docs",
"/favicon.ico"
};
// 配置各請(qǐng)求路徑的認(rèn)證與授權(quán)
http.authorizeRequests() // 請(qǐng)求需要授權(quán)才可以訪(fǎng)問(wèn)
.antMatchers(urls) // 匹配一些路徑
.permitAll() // 允許直接訪(fǎng)問(wèn)(不需要經(jīng)過(guò)認(rèn)證和授權(quán))
.anyRequest() // 匹配除了以上配置的其它請(qǐng)求
.authenticated(); // 都需要認(rèn)證
// 注冊(cè)處理JWT的過(guò)濾器
// 此過(guò)濾器必須在Spring Security處理登錄的過(guò)濾器之前
http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}
我們重起項(xiàng)目,輸入之前的url,不太對(duì)啊,下載了一個(gè)空的login文件,控制臺(tái)看到了如下內(nèi)容:
JwtAuthenticationFilter.doFilterInternal()
那是因?yàn)檫^(guò)濾器的工作還沒(méi)有結(jié)束,他還需要實(shí)現(xiàn)以下功能:
- 嘗試從請(qǐng)求頭中獲取JWT數(shù)據(jù),如果無(wú)JWT數(shù)據(jù),直接放行,Spring Security會(huì)進(jìn)行下一步處理,比如,白名單的請(qǐng)求允許訪(fǎng)問(wèn),其它請(qǐng)求禁止訪(fǎng)問(wèn)
- 如果存在JWT數(shù)據(jù),應(yīng)該嘗試解析,解析失敗,就是認(rèn)證失敗了,要求客戶(hù)端重新登錄,客戶(hù)端就可以得到新的、正確的JWT,客戶(hù)端在下一次提交請(qǐng)求時(shí),使用新的JWT就可以正常訪(fǎng)問(wèn)
- 將解析得到的數(shù)據(jù)封裝到
Authentication
對(duì)象中,Spring Security的上下文中存儲(chǔ)的數(shù)據(jù)類(lèi)型就是Authentication
類(lèi)型 - 為避免存入1次后,Spring Security的上下文中始終存在
Authentication
,在此過(guò)濾器執(zhí)行的第一時(shí)間,應(yīng)該先清除上一次的數(shù)據(jù)
?下面,我們來(lái)看看自定義過(guò)濾器中還有哪些代碼:
package com.codingfire.cloud.passport.security;
import com.alibaba.fastjson.JSON;
import com.codingfire.cloud.commons.restful.JsonResult;
import com.codingfire.cloud.commons.restful.ResponseCode;
import io.jsonwebtoken.*;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
/**
* JWT過(guò)濾器:從請(qǐng)求頭的Authorization中獲取JWT中存入的用戶(hù)信息
* 并添加到Spring Security的上下文中
* 以致于Spring Security后續(xù)的組件(包括過(guò)濾器等)能從上下文中獲取此用戶(hù)的信息
* 從而驗(yàn)證是否已經(jīng)登錄、是否具有權(quán)限等
*/
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
/**
* JWT數(shù)據(jù)的密鑰
*/
private String secretKey = "fgfdsfadsfadsafdsafdsfadsfadsfdsafdasfdsafdsafdsafds4rttrefds";
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
System.out.println("JwtAuthenticationFilter.doFilterInternal()");
// 清除Spring Security上下文中的數(shù)據(jù)
// 避免此前曾經(jīng)存入過(guò)用戶(hù)信息,后續(xù)即使沒(méi)有攜帶JWT,在Spring Security仍保存有上下文數(shù)據(jù)(包括用戶(hù)信息)
System.out.println("清除Spring Security上下文中的數(shù)據(jù)");
SecurityContextHolder.clearContext();
// 客戶(hù)端提交請(qǐng)求時(shí),必須在請(qǐng)求頭的Authorization中添加JWT數(shù)據(jù),這是當(dāng)前服務(wù)器程序的規(guī)定,客戶(hù)端必須遵守
// 嘗試獲取JWT數(shù)據(jù)
String jwt = request.getHeader("Authorization");
System.out.println("從請(qǐng)求頭中獲取到的JWT=" + jwt);
// 判斷是否不存在jwt數(shù)據(jù)
if (!StringUtils.hasText(jwt)) {
// 不存在jwt數(shù)據(jù),則放行,后續(xù)還有其它過(guò)濾器及相關(guān)組件進(jìn)行其它的處理,例如未登錄則要求登錄等
// 此處不宜直接阻止運(yùn)行,因?yàn)椤暗卿洝?、“注?cè)”等請(qǐng)求本應(yīng)該沒(méi)有jwt數(shù)據(jù)
System.out.println("請(qǐng)求頭中無(wú)JWT數(shù)據(jù),當(dāng)前過(guò)濾器將放行");
filterChain.doFilter(request, response); // 繼續(xù)執(zhí)行過(guò)濾器鏈中后續(xù)的過(guò)濾器
return; // 必須
}
// 注意:此時(shí)執(zhí)行時(shí),如果請(qǐng)求頭中攜帶了Authentication,日志中將輸出,且不會(huì)有任何響應(yīng),因?yàn)楫?dāng)前過(guò)濾器尚未放行
// 以下代碼有可能拋出異常的
// TODO 密鑰和各個(gè)Key應(yīng)該統(tǒng)一定義
String username = null;
String permissionsString = null;
try {
System.out.println("請(qǐng)求頭中包含JWT,準(zhǔn)備解析此數(shù)據(jù)……");
Claims claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt).getBody();
username = claims.get("username").toString();
permissionsString = claims.get("permissions").toString();
System.out.println("username=" + username);
System.out.println("permissionsString=" + permissionsString);
} catch (ExpiredJwtException e) {
System.out.println("解析JWT失敗,此JWT已過(guò)期:" + e.getMessage());
JsonResult<Void> jsonResult = JsonResult.failed(
ResponseCode.ERR_JWT_EXPIRED, "您的登錄已過(guò)期,請(qǐng)重新登錄!");
String jsonString = JSON.toJSONString(jsonResult);
System.out.println("響應(yīng)結(jié)果:" + jsonString);
response.setContentType("application/json; charset=utf-8");
response.getWriter().println(jsonString);
return;
} catch (MalformedJwtException e) {
System.out.println("解析JWT失敗,此JWT數(shù)據(jù)錯(cuò)誤,無(wú)法解析:" + e.getMessage());
JsonResult<Void> jsonResult = JsonResult.failed(
ResponseCode.ERR_JWT_MALFORMED, "獲取登錄信息失敗,請(qǐng)重新登錄!");
String jsonString = JSON.toJSONString(jsonResult);
System.out.println("響應(yīng)結(jié)果:" + jsonString);
response.setContentType("application/json; charset=utf-8");
response.getWriter().println(jsonString);
return;
} catch (SignatureException e) {
System.out.println("解析JWT失敗,此JWT簽名錯(cuò)誤:" + e.getMessage());
JsonResult<Void> jsonResult = JsonResult.failed(
ResponseCode.ERR_JWT_SIGNATURE, "獲取登錄信息失敗,請(qǐng)重新登錄!");
String jsonString = JSON.toJSONString(jsonResult);
System.out.println("響應(yīng)結(jié)果:" + jsonString);
response.setContentType("application/json; charset=utf-8");
response.getWriter().println(jsonString);
return;
} catch (Throwable e) {
System.out.println("解析JWT失敗,異常類(lèi)型:" + e.getClass().getName());
e.printStackTrace();
JsonResult<Void> jsonResult = JsonResult.failed(
ResponseCode.ERR_INTERNAL_SERVER_ERROR, "獲取登錄信息失敗,請(qǐng)重新登錄!");
String jsonString = JSON.toJSONString(jsonResult);
System.out.println("響應(yīng)結(jié)果:" + jsonString);
response.setContentType("application/json; charset=utf-8");
response.getWriter().println(jsonString);
return;
}
// 將此前從JWT中讀取到的permissionsString(JSON字符串)轉(zhuǎn)換成Collection<? extends GrantedAuthority>
List<SimpleGrantedAuthority> permissions
= JSON.parseArray(permissionsString, SimpleGrantedAuthority.class);
System.out.println("從JWT中獲取到的權(quán)限轉(zhuǎn)換成Spring Security要求的類(lèi)型:" + permissions);
// 將解析得到的用戶(hù)信息傳遞給Spring Security
// 獲取Spring Security的上下文,并將Authentication放到上下文中
// 在Authentication中封裝:用戶(hù)名、null(密碼)、權(quán)限列表
// 因?yàn)榻酉聛?lái)并不會(huì)處理認(rèn)證,所以Authentication中不需要密碼
// 后續(xù),Spring Security發(fā)現(xiàn)上下文中有Authentication時(shí),就會(huì)視為已登錄,甚至可以獲取相關(guān)信息
Authentication authentication
= new UsernamePasswordAuthenticationToken(username, null, permissions);
SecurityContextHolder.getContext().setAuthentication(authentication);
System.out.println("將解析得到的用戶(hù)信息傳遞給Spring Security");
// 放行
System.out.println("JwtAuthenticationFilter 放行");
filterChain.doFilter(request, response);
}
}
你可能在添加了這個(gè)類(lèi)中的代碼后會(huì)有一些報(bào)錯(cuò),是因?yàn)殄e(cuò)誤碼沒(méi)有提前聲明在枚舉類(lèi),自己手動(dòng)添加一下。
接著在SecurityConfiguration類(lèi)上添加一個(gè)新的注解
@EnableGlobalMethodSecurity(prePostEnabled = true) // 新增
作用是開(kāi)啟“通過(guò)注解配置權(quán)限”的功能。
下面,我們來(lái)做個(gè)測(cè)試,在任何你需要設(shè)置權(quán)限的處理請(qǐng)求的方法上,通過(guò)@PreAuthorize
注解來(lái)實(shí)現(xiàn)通過(guò)注解配置權(quán)限功能,你可以配置你想要的某種權(quán)限:
在AdminController類(lèi)中添加如下方法:
@GetMapping("/codingfire")
@PreAuthorize("hasAuthority('單頻道觀看')") // 新增
public String codingfire() {
return "codingfire";
}
重啟項(xiàng)目,使用具有“單頻道觀看”
權(quán)限的用戶(hù)可以直接訪(fǎng)問(wèn),不具有此權(quán)限的用戶(hù)則不能訪(fǎng)問(wèn),將出現(xiàn)403錯(cuò)誤,可通過(guò)在線(xiàn)文檔功能進(jìn)行測(cè)試。
在線(xiàn)文檔添加請(qǐng)求頭方式:
請(qǐng)求頭內(nèi)的數(shù)據(jù)使用正常用的登錄后返回的JWT數(shù)據(jù),登錄的用戶(hù)權(quán)限可自己調(diào)整,然后訪(fǎng)問(wèn)codingfire接口查看結(jié)果。博主就不再貼后續(xù)的內(nèi)容了。?文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-782229.html
結(jié)語(yǔ)
雖然這篇博客結(jié)束了,但登錄并沒(méi)有結(jié)束,登錄的整體邏輯還有不少,關(guān)鍵的部分本文已經(jīng)全部列出,剩下的就要大家在實(shí)戰(zhàn)中慢慢疊加了。3w字才碼完,可以說(shuō)是自己又學(xué)習(xí)了一遍,你會(huì)發(fā)現(xiàn),很多東西都是套路的固定的,只有少部分東西是需要自己去寫(xiě)的,那就是涉及業(yè)務(wù)的部分。希望大家都能有所收獲。文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-782229.html
到了這里,關(guān)于Java開(kāi)發(fā) - 單點(diǎn)登錄初體驗(yàn)(Spring Security + JWT)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!