Spring Boot+Spring Security+JWT實(shí)現(xiàn)單點(diǎn)登錄
一.概念
1.1.SSO
介紹:
-
單點(diǎn)登錄(SingleSignOn,SSO),當(dāng)用戶在身份
認(rèn)證服務(wù)器
上登錄一次以后,即可獲得訪問(wèn)單點(diǎn)登錄系統(tǒng)中其他關(guān)聯(lián)系統(tǒng)和應(yīng)用軟件的權(quán)限,同時(shí)這種實(shí)現(xiàn)是不需要管理員對(duì)用戶的登錄狀態(tài)或其他信息進(jìn)行修改的,這意味著在多個(gè)應(yīng)用系統(tǒng)中,用戶只需一次登錄就可以訪問(wèn)所有相互信任的應(yīng)用系統(tǒng)
。這種方式減少了由登錄產(chǎn)生的時(shí)間消耗,輔助了用戶管理,是目前比較流行的一種分布式登錄方式。
SSO實(shí)現(xiàn)流程:
- 在分布式項(xiàng)目中,
每臺(tái)服務(wù)器都有各自獨(dú)立的session,而這些session之間是無(wú)法直接共享資源的
,所以,session通常不能被作為單點(diǎn)登錄的技術(shù)方案。最合理的單點(diǎn)登錄方案流程如下圖所示:
單點(diǎn)登錄的實(shí)現(xiàn)分2部分:
-
用戶認(rèn)證:客戶端向認(rèn)證服務(wù)器發(fā)起認(rèn)證請(qǐng)求,認(rèn)證服務(wù)器給客戶端返回令牌token, 主要在
認(rèn)證服務(wù)器
中完成,即圖中的A系統(tǒng),注意認(rèn)證服務(wù)器只能有一個(gè)
-
身份校驗(yàn): 客戶端攜帶token去訪問(wèn)其他資源服務(wù)器時(shí),在資源服務(wù)器中要對(duì)token的真?zhèn)芜M(jìn)行檢驗(yàn),主要在
資源服務(wù)器
中完成,即圖中的B系統(tǒng),這里B系統(tǒng)可以有很多個(gè)
1.2.JWT
什么是JWT
- 【JavaWeb】關(guān)于JWT做認(rèn)證授權(quán)的十萬(wàn)個(gè)理由(JSON Web Token)
1.3.RSA
非對(duì)稱加密算法
- 服務(wù)提供方生成兩把密鑰(公鑰和私鑰)。私鑰隱秘保存,公鑰公開,下發(fā)給信任客戶端
- 調(diào)用方獲取提供方的公鑰,然后用它對(duì)信息加密。
- 提供方接收到調(diào)用加密后的信息后,用私鑰解密。
RSA算法
- 一直是最廣為使用的"非對(duì)稱加密算法"。毫不夸張地說(shuō),只要有計(jì)算機(jī)網(wǎng)絡(luò)的地方,就有RSA算法。這種算法非常可靠,密鑰越長(zhǎng),它就越難破解。根據(jù)已經(jīng)披露的文獻(xiàn),目前被破解的最長(zhǎng)RSA密鑰是768個(gè)二進(jìn)制位。也就是說(shuō),
長(zhǎng)度超過(guò)768位的密鑰,還無(wú)法破解(至少?zèng)]人公開宣布)。因此可以認(rèn)為,1024位的RSA密鑰基本安全,2048位的密鑰極其安全。
RSA使用流程:
-
生成兩把密鑰:私鑰和公鑰,私鑰保存起來(lái),公鑰可以下發(fā)給信任客戶端
-
私鑰加密,
持有私鑰或公鑰才可以解密
-
公鑰加密,
持有私鑰才可解密
-
私鑰加密,
- 因此,認(rèn)證服務(wù)一般存放
私鑰和公鑰
,而資源服務(wù)一般存放公鑰
。私鑰負(fù)責(zé)加密,公鑰負(fù)責(zé)解密。
二.思路
1.分析集中式認(rèn)證流程
-
用戶認(rèn)證:使用
UsernamePasswordAuthenticationFilter
過(guò)濾器中attemptAuthentication()
實(shí)現(xiàn)認(rèn)證功能,該過(guò)濾器父類中successfulAuthentication()
實(shí)現(xiàn)認(rèn)證成功后的操作。 -
身份校驗(yàn):使用
BasicAuthenticationFilter
過(guò)濾器中doFilterInternal()
驗(yàn)證是否登錄,以決定能否進(jìn)入后續(xù)過(guò)濾器。
2.分析分布式認(rèn)證流程
-
用戶認(rèn)證:分布式項(xiàng)目多數(shù)是
前后端分離
的架構(gòu),需要修改UsernamePasswordAuthenticationFilter
過(guò)濾器中attemptAuthentication()
,讓其能夠接收請(qǐng)求體。另外,默認(rèn)successfulAuthentication()
在認(rèn)證通過(guò)后,是把用戶信息直接放入session
就完事了-
處理方式:修改
successfulAuthentication()
,在認(rèn)證通過(guò)后生成token并返回給用戶。
-
處理方式:修改
-
身份校驗(yàn): 原來(lái)
BasicAuthenticationFilter
過(guò)濾器中doFilterInternal()
校驗(yàn)用戶是否登錄,就是看session
中是否有用戶信息-
處理方式:校驗(yàn)邏輯修改為,驗(yàn)證用戶攜帶的
token
合法,并解析出用戶信息
,交給SpringSecurity,以便于后續(xù)的授權(quán)功能可以正常使用。
-
處理方式:校驗(yàn)邏輯修改為,驗(yàn)證用戶攜帶的
//Header.Payload.Signature
HMACSHA245(base64UrlEncode(header)+"."+base64UrlEncode(payload),secret)
三.工程介紹
1.介紹父工程
因?yàn)楸景咐枰獎(jiǎng)?chuàng)建多個(gè)系統(tǒng),所以我們使用maven聚合工程
來(lái)實(shí)現(xiàn),首先創(chuàng)建一個(gè)父工程,導(dǎo)入springboot的父依賴即可
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.2</version>
<relativePath/>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>com.oyjp</groupId>
<artifactId>spring-boot-security-sso-parent</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<description>通用模塊</description>
<modules>
<module>sso-common</module><!--通用子模塊-->
<module>sso-auth-server</module><!--認(rèn)證服務(wù)子模塊-->
<module>sso-source-product</module><!--產(chǎn)品資源服務(wù)子模塊-->
<module>sso-source-order</module><!--訂單資源服務(wù)子模塊-->
</modules>
該工程由四個(gè)子模塊組成,一個(gè)認(rèn)證服務(wù)模塊,一個(gè)通用模塊,一個(gè)訂單資源模塊,一個(gè)產(chǎn)品資源模塊
2.導(dǎo)入數(shù)據(jù)庫(kù)
DROP DATABASE IF EXISTS `security_test2`;
CREATE DATABASE `security_test2`;
USE `security_test2`;
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role` (
`id` INT(11) NOT NULL AUTO_INCREMENT COMMENT '角色編號(hào)',
`name` VARCHAR(32) NOT NULL COMMENT '角色名稱',
`desc` VARCHAR(32) NOT NULL COMMENT '角色描述',
PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
INSERT INTO `sys_role`(`id`,`name`,`desc`) VALUES (1,'ROLE_USER','用戶權(quán)限');
INSERT INTO `sys_role`(`id`,`name`,`desc`) VALUES (2,'ROLE_ADMIN','管理權(quán)限');
INSERT INTO `sys_role`(`id`,`name`,`desc`) VALUES (3,'ROLE_PRODUCT','產(chǎn)品權(quán)限');
INSERT INTO `sys_role`(`id`,`name`,`desc`) VALUES (4,'ROLE_ORDER','訂單權(quán)限');
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (
`id` INT(11) NOT NULL AUTO_INCREMENT COMMENT '用戶編號(hào)',
`username` VARCHAR(32) NOT NULL COMMENT '用戶名稱',
`password` VARCHAR(128) NOT NULL COMMENT '用戶密碼',
`status` INT(1) NOT NULL DEFAULT '1' COMMENT '用戶狀態(tài)(0:關(guān)閉、1:開啟)',
PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
INSERT INTO `sys_user`(`id`,`username`,`password`,`status`) VALUES (1,'zhangsan','$2a$10$M7fmKpMZEkkzrTBiKie.EeAKZhQDrWAltpCA1y/py5AU/8lyiNB8y',0);
INSERT INTO `sys_user`(`id`,`username`,`password`,`status`) VALUES (2,'lisi','$2a$10$M7fmKpMZEkkzrTBiKie.EeAKZhQDrWAltpCA1y/py5AU/8lyiNB8y',1);
INSERT INTO `sys_user`(`id`,`username`,`password`,`status`) VALUES (3,'wangwu','$2a$10$M7fmKpMZEkkzrTBiKie.EeAKZhQDrWAltpCA1y/py5AU/8lyiNB8y',2);
INSERT INTO `sys_user`(`id`,`username`,`password`,`status`) VALUES (4,'zhaoliu','$2a$10$M7fmKpMZEkkzrTBiKie.EeAKZhQDrWAltpCA1y/py5AU/8lyiNB8y',3);
INSERT INTO `sys_user`(`id`,`username`,`password`,`status`) VALUES (5,'xiaoqi','$2a$10$M7fmKpMZEkkzrTBiKie.EeAKZhQDrWAltpCA1y/py5AU/8lyiNB8y',4);
DROP TABLE IF EXISTS `sys_user_role`;
CREATE TABLE `sys_user_role` (
`uid` INT(11) NOT NULL COMMENT '用戶編號(hào)',
`rid` INT(11) NOT NULL COMMENT '角色編號(hào)',
PRIMARY KEY (`uid`,`rid`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;
INSERT INTO `sys_user_role`(`uid`,`rid`) VALUES (1,1);
INSERT INTO `sys_user_role`(`uid`,`rid`) VALUES (1,3);
INSERT INTO `sys_user_role`(`uid`,`rid`) VALUES (2,1);
INSERT INTO `sys_user_role`(`uid`,`rid`) VALUES (2,4);
INSERT INTO `sys_user_role`(`uid`,`rid`) VALUES (3,1);
INSERT INTO `sys_user_role`(`uid`,`rid`) VALUES (3,2);
INSERT INTO `sys_user_role`(`uid`,`rid`) VALUES (3,3);
INSERT INTO `sys_user_role`(`uid`,`rid`) VALUES (3,4);
四 通用模塊
1.導(dǎo)入依賴
<!--JWT-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.2</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.2</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.2</version>
<scope>runtime</scope>
</dependency>
<!--Jackson-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.11.4</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.11.4</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.11.4</version>
</dependency>
<!--JodaTime-->
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>2.10.9</version>
</dependency>
<!--Lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.16</version>
</dependency>
<!--日志包-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
<!--測(cè)試包-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
</dependency>
2.統(tǒng)一格式
2.1.統(tǒng)一載荷對(duì)象
/**
* 為了方便后期獲取token中的用戶信息,將token中載荷部分單獨(dú)封裝成一個(gè)對(duì)象
* @author JianpengOuYang
*/
@Data
public class Payload<T> implements Serializable {
private String id;
private T userInfo;
private Date expiration;
}
2.2.統(tǒng)一返回結(jié)果
/**
* 統(tǒng)一處理返回結(jié)果
* @author JianpengOuYang
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result implements Serializable {
private Integer code;
private String msg;
private Object data;
}
3.常用工具
3.1.Json工具類
/**
* 對(duì)Jackson中的方法進(jìn)行了簡(jiǎn)單封裝
* @author JianpengOuYang
*/
public class JsonUtils {
private static final Logger logger = LoggerFactory.getLogger(JsonUtils.class);
private static final ObjectMapper mapper = new ObjectMapper();
/**
* 將指定對(duì)象序列化為一個(gè)json字符串
*
* @param obj 指定對(duì)象
* @return 返回一個(gè)json字符串
*/
public static String toString(Object obj) {
if (obj == null) {
return null;
}
if (obj.getClass() == String.class) {
return (String) obj;
}
try {
return mapper.writeValueAsString(obj);
} catch (JsonProcessingException e) {
logger.error("json序列化出錯(cuò):" + obj, e);
return null;
}
}
/**
* 將指定json字符串解析為指定類型對(duì)象
*
* @param json json字符串
* @param tClass 指定類型
* @return 返回一個(gè)指定類型對(duì)象
*/
public static <T> T toBean(String json, Class<T> tClass) {
try {
return mapper.readValue(json, tClass);
} catch (IOException e) {
logger.error("json解析出錯(cuò):" + json, e);
return null;
}
}
/**
* 將指定輸入流解析為指定類型對(duì)象
*
* @param inputStream 輸入流對(duì)象
* @param tClass 指定類型
* @return 返回一個(gè)指定類型對(duì)象
*/
public static <T> T toBean(InputStream inputStream, Class<T> tClass) {
try {
return mapper.readValue(inputStream, tClass);
} catch (IOException e) {
logger.error("json解析出錯(cuò):" + inputStream, e);
return null;
}
}
/**
* 將指定json字符串解析為指定類型集合
*
* @param json json字符串
* @param eClass 指定元素類型
* @return 返回一個(gè)指定類型集合
*/
public static <E> List<E> toList(String json, Class<E> eClass) {
try {
return mapper.readValue(json, mapper.getTypeFactory().constructCollectionType(List.class, eClass));
} catch (IOException e) {
logger.error("json解析出錯(cuò):" + json, e);
return null;
}
}
/**
* 將指定json字符串解析為指定鍵值對(duì)類型集合
*
* @param json json字符串
* @param kClass 指定鍵類型
* @param vClass 指定值類型
* @return 返回一個(gè)指定鍵值對(duì)類型集合
*/
public static <K, V> Map<K, V> toMap(String json, Class<K> kClass, Class<V> vClass) {
try {
return mapper.readValue(json, mapper.getTypeFactory().constructMapType(Map.class, kClass, vClass));
} catch (IOException e) {
logger.error("json解析出錯(cuò):" + json, e);
return null;
}
}
/**
* 將指定json字符串解析為一個(gè)復(fù)雜類型對(duì)象
*
* @param json json字符串
* @param type 復(fù)雜類型
* @return 返回一個(gè)復(fù)雜類型對(duì)象
*/
public static <T> T nativeRead(String json, TypeReference<T> type) {
try {
return mapper.readValue(json, type);
} catch (IOException e) {
logger.error("json解析出錯(cuò):" + json, e);
return null;
}
}
}
3.2.Jwt工具類
/**
* 生成token以及校驗(yàn)token相關(guān)方法
*
* @author JianpengOuYang
*/
public class JwtUtils {
private static final String JWT_PAYLOAD_USER_KEY = "user";
private static String createJTI() {
return new String(Base64.getEncoder().encode(UUID.randomUUID().toString().getBytes()));
}
/**
* 私鑰加密token
*
* @param userInfo 載荷中的數(shù)據(jù)
* @param privateKey 私鑰
* @param expire 過(guò)期時(shí)間,單位分鐘
* @return JWT
*/
public static String generateTokenExpireInMinutes(Object userInfo, PrivateKey privateKey, int expire) {
return Jwts.builder()
.claim(JWT_PAYLOAD_USER_KEY, JsonUtils.toString(userInfo))//payload
.setId(createJTI())//JID
.setExpiration(DateTime.now().plusMinutes(expire).toDate())//過(guò)期時(shí)間
.signWith(privateKey, SignatureAlgorithm.RS256)//Signature,使用privateKey作為密鑰
.compact();
}
/**
* 私鑰加密token
*
* @param userInfo 載荷中的數(shù)據(jù)
* @param privateKey 私鑰
* @param expire 過(guò)期時(shí)間,單位秒
* @return JWT
*/
public static String generateTokenExpireInSeconds(Object userInfo, PrivateKey privateKey, int expire) {
return Jwts.builder()
.claim(JWT_PAYLOAD_USER_KEY, JsonUtils.toString(userInfo))
.setId(createJTI())
.setExpiration(DateTime.now().plusSeconds(expire).toDate())
.signWith(privateKey, SignatureAlgorithm.RS256)
.compact();
}
/**
* 公鑰解析token
*
* @param token 用戶請(qǐng)求中的token
* @param publicKey 公鑰
* @return Jws<Claims>
*/
private static Jws<Claims> parserToken(String token, PublicKey publicKey) {
return Jwts.parserBuilder()
.setSigningKey(publicKey)
.build()
.parseClaimsJws(token);
}
/**
* 獲取token中的用戶信息
*
* @param token 用戶請(qǐng)求中的令牌
* @param publicKey 公鑰
* @return 用戶信息
*/
public static <T> Payload<T> getInfoFromToken(String token, PublicKey publicKey, Class<T> userType) {
Jws<Claims> claimsJws = parserToken(token, publicKey);
Claims body = claimsJws.getBody();
Payload<T> claims = new Payload<>();
claims.setId(body.getId());//JID
claims.setUserInfo(JsonUtils.toBean(body.get(JWT_PAYLOAD_USER_KEY).toString(), userType));//獲取payload中的用戶信息
claims.setExpiration(body.getExpiration());//獲取過(guò)期時(shí)間
return claims;
}
/**
* 獲取token中的載荷信息
*
* @param token 用戶請(qǐng)求中的令牌
* @param publicKey 公鑰
* @return 用戶信息
*/
public static <T> Payload<T> getInfoFromToken(String token, PublicKey publicKey) {
Jws<Claims> claimsJws = parserToken(token, publicKey);
Claims body = claimsJws.getBody();
Payload<T> claims = new Payload<>();
claims.setId(body.getId());
claims.setExpiration(body.getExpiration());
return claims;
}
}
3.3.Rsa工具類
/**
* 對(duì)Rsa操作進(jìn)行了簡(jiǎn)單封裝
*
* @author JianpengOuYang
*/
public class RsaUtils {
private static final int DEFAULT_KEY_SIZE = 2048;
/**
* 從文件中讀取公鑰
*
* @param filename 公鑰保存路徑,相對(duì)于classpath
* @return 公鑰對(duì)象
* @throws Exception
*/
public static PublicKey getPublicKey(String filename) throws Exception {
byte[] bytes = readFile(filename);
return getPublicKey(bytes);
}
/**
* 從文件中讀取密鑰
*
* @param filename 私鑰保存路徑,相對(duì)于classpath
* @return 私鑰對(duì)象
* @throws Exception
*/
public static PrivateKey getPrivateKey(String filename) throws Exception {
byte[] bytes = readFile(filename);
return getPrivateKey(bytes);
}
/**
* 獲取公鑰
*
* @param bytes 公鑰的字節(jié)形式
* @return
* @throws Exception
*/
private static PublicKey getPublicKey(byte[] bytes) throws Exception {
bytes = Base64.getDecoder().decode(bytes);
X509EncodedKeySpec spec = new X509EncodedKeySpec(bytes);
KeyFactory factory = KeyFactory.getInstance("RSA");
return factory.generatePublic(spec);
}
/**
* 獲取密鑰
*
* @param bytes 私鑰的字節(jié)形式
* @return
* @throws Exception
*/
private static PrivateKey getPrivateKey(byte[] bytes) throws NoSuchAlgorithmException, InvalidKeySpecException {
bytes = Base64.getDecoder().decode(bytes);
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(bytes);
KeyFactory factory = KeyFactory.getInstance("RSA");
return factory.generatePrivate(spec);
}
/**
* 根據(jù)密文,生成rsa公鑰和私鑰,并寫入指定文件
*
* @param publicKeyFilename 公鑰文件路徑
* @param privateKeyFilename 私鑰文件路徑
* @param secret 生成密鑰的密文
*/
public static void generateKey(String publicKeyFilename, String privateKeyFilename, String secret, int keySize) throws Exception {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
SecureRandom secureRandom = new SecureRandom(secret.getBytes());
keyPairGenerator.initialize(Math.max(keySize, DEFAULT_KEY_SIZE), secureRandom);
KeyPair keyPair = keyPairGenerator.genKeyPair();
// 獲取公鑰并寫出
byte[] publicKeyBytes = keyPair.getPublic().getEncoded();
publicKeyBytes = Base64.getEncoder().encode(publicKeyBytes);
writeFile(publicKeyFilename, publicKeyBytes);
// 獲取私鑰并寫出
byte[] privateKeyBytes = keyPair.getPrivate().getEncoded();
privateKeyBytes = Base64.getEncoder().encode(privateKeyBytes);
writeFile(privateKeyFilename, privateKeyBytes);
}
private static byte[] readFile(String fileName) throws Exception {
return Files.readAllBytes(new File(fileName).toPath());
}
private static void writeFile(String destPath, byte[] bytes) throws IOException {
File dest = new File(destPath);
File parentFile = dest.getParentFile();
if (!parentFile.exists()) {
parentFile.mkdirs();
}
if (!dest.exists()) {
dest.createNewFile();
}
Files.write(dest.toPath(), bytes);
}
}
3.4.Response/Request工具類
/**
* 請(qǐng)求工具類
*
* @author oyjp
*/
public class RequestUtils {
private static final Logger logger = LoggerFactory.getLogger(RequestUtils.class);
/**
* 從請(qǐng)求對(duì)象的輸入流中獲取指定類型對(duì)象
*
* @param request 請(qǐng)求對(duì)象
* @param clazz 指定類型
* @return 指定類型對(duì)象
*/
public static <T> T read(HttpServletRequest request, Class<T> clazz) {
try {
return JsonUtils.toBean(request.getInputStream(), clazz);
} catch (Exception e) {
logger.error("讀取出錯(cuò):" + clazz, e);
return null;
}
}
}
/**
* 響應(yīng)工具類
*
* @author oyjp
*/
public class ResponseUtils {
private static final Logger logger = LoggerFactory.getLogger(ResponseUtils.class);
/**
* 向?yàn)g覽器響應(yīng)一個(gè)json字符串
*
* @param response 響應(yīng)對(duì)象
* @param status 狀態(tài)碼
* @param msg 響應(yīng)信息
*/
public static void write(HttpServletResponse response, int status, String msg) {
try {
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Cache-Control", "no-cache");
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
response.setStatus(status);
byte[] bytes = JsonUtils.toString(new Result(status, msg, null)).getBytes();
OutputStream out = response.getOutputStream();
out.write(bytes);
} catch (Exception e) {
logger.error("響應(yīng)出錯(cuò):" + msg, e);
}
}
}
4.生成密鑰
- 使用密鑰在指定位置生成公鑰/私鑰文件
public class RsaUtilsTest {
private String publicFile = "E:\\auth_key\\rsa_key.pub";
private String privateFile = "E:\\auth_key\\rsa_key";
private String secret = "JianpengOuYangSecret";
@Test
public void generateKey() throws Exception {
RsaUtils.generateKey(publicFile, privateFile, secret, 2048);
}
}
五 認(rèn)證服務(wù)
注意:本章節(jié)所有操作均在
sso-auth-server
中進(jìn)行。
1.導(dǎo)入依賴
<dependencies>
<!--web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--springSecurity-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--mybatis、mysql-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.49</version>
</dependency>
<!--引入通用子模塊-->
<dependency>
<groupId>com.oyjp</groupId>
<artifactId>sso-common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
2.創(chuàng)建配置文件
server:
port: 9001
servlet:
application-display-name: sso-auth-server
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/security_test2?useSSL=false
username: root
password: root
mybatis:
type-aliases-package: com.oyjp.domain
configuration:
map-underscore-to-camel-case: true
logging:
level:
com.oyjp: debug
#自定義屬性,配置私鑰路徑
rsa:
key:
privateKeyPath: E:\auth_key\rsa_key
3.編寫讀取公鑰的配置類
@Data
@ConfigurationProperties(prefix = "rsa.key", ignoreInvalidFields = true)
public class RsaKeyProperties {
private String publicKeyPath;
private String privateKeyPath;
private PublicKey publicKey;
private PrivateKey privateKey;
/**
* 該方法用于初始化公鑰和私鑰的內(nèi)容
*/
@PostConstruct
public void loadRsaKey() throws Exception {
if (publicKeyPath != null) {
publicKey = RsaUtils.getPublicKey(publicKeyPath);
}
if (privateKeyPath != null) {
privateKey = RsaUtils.getPrivateKey(privateKeyPath);
}
}
}
4.編寫啟動(dòng)類
@SpringBootApplication
@EnableConfigurationProperties(RsaKeyProperties.class) //啟動(dòng)時(shí)加載配置類
public class AuthServerApplication {
public static void main(String[] args) {
SpringApplication.run(AuthServerApplication.class, args);
}
}
5.編寫實(shí)體類
用戶類實(shí)現(xiàn)springSecurity的UserDetails 接口
@Data
public class SysUser implements UserDetails {
private Integer id;
private String username;
private String password;
private Integer status;
private List<SysRole> sysRoles;
@JsonIgnore
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return sysRoles;
}
/**
* 是否賬號(hào)已過(guò)期
*/
@JsonIgnore
@Override
public boolean isAccountNonExpired() {
return status != 1;
}
/**
* 是否賬號(hào)已被鎖
*/
@JsonIgnore
@Override
public boolean isAccountNonLocked() {
return status != 2;
}
/**
* 是否憑證已過(guò)期
*/
@JsonIgnore
@Override
public boolean isCredentialsNonExpired() {
return status != 3;
}
/**
* 是否賬號(hào)已禁用
*/
@JsonIgnore
@Override
public boolean isEnabled() {
return status != 4;
}
}
角色類實(shí)現(xiàn)springSecurity的GrantedAuthority接口
@Data
public class SysRole implements GrantedAuthority {
private Integer id;
private String name;
private String desc;
@JsonIgnore
@Override
public String getAuthority() {
return name;
}
}
6.編寫映射接口
查用戶信息
@Mapper
public interface SysUserMapper {
//根據(jù)用戶名稱查詢所對(duì)應(yīng)的用戶信息
@Select("select * from `sys_user` where `username` = #{username}")
@Results({
//主鍵字段映射,property代表Java對(duì)象屬性,column代表數(shù)據(jù)庫(kù)字段
@Result(property = "id", column = "id", id = true),
//普通字段映射,property代表Java對(duì)象屬性,column代表數(shù)據(jù)庫(kù)字段
@Result(property = "username", column = "username"),
@Result(property = "password", column = "password"),
@Result(property = "status", column = "status"),
//角色列表映射,根據(jù)用戶id查詢?cè)撚脩羲鶎?duì)應(yīng)的角色列表sysRoles
@Result(property = "sysRoles", column = "id",
javaType = List.class,
many = @Many(select = "com.oyjp.mapper.SysRoleMapper.findByUid")
)
})
SysUser findByUsername(String username);
}
查角色信息
@Mapper
public interface SysRoleMapper {
//根據(jù)用戶編號(hào)查詢角色列表
@Select("select * from `sys_role` where id in (" +
" select rid from `sys_user_role` where uid = #{uid}" +
")")
List<SysRole> findByUid(Integer uid);
}
7.編寫服務(wù)接口
實(shí)現(xiàn)springSecurity的UserDetailsService 接口,重新loadUserByUsername()
public interface SysUserDetailsService extends UserDetailsService {
}
@Service
@Transactional
public class SysUserDetailsServiceImpl implements SysUserDetailsService {
@Autowired(required = false)
private SysUserMapper sysUserMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//根據(jù)用戶名去數(shù)據(jù)庫(kù)中查詢指定用戶,這就要保證數(shù)據(jù)庫(kù)中的用戶的名稱必須唯一,否則將會(huì)報(bào)錯(cuò)
SysUser sysUser = sysUserMapper.findByUsername(username);
//如果沒(méi)有查詢到這個(gè)用戶,說(shuō)明數(shù)據(jù)庫(kù)中不存在此用戶,認(rèn)證失敗,此時(shí)需要拋出用戶賬戶不存在
if (sysUser == null) {
throw new UsernameNotFoundException("user not exist.");
}
return sysUser;
}
}
8.編寫認(rèn)證過(guò)濾器
/**
* 認(rèn)證過(guò)濾器
*
*/
@Slf4j
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
private RsaKeyProperties prop;
public JwtAuthenticationFilter(AuthenticationManager authenticationManager, RsaKeyProperties prop) {
this.authenticationManager = authenticationManager;
this.prop = prop;
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
SysUser sysUser = RequestUtils.read(request, SysUser.class);
assert sysUser != null;
String username = sysUser.getUsername();
username = username != null ? username : "";
String password = sysUser.getPassword();
password = password != null ? password : "";
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
return authenticationManager.authenticate(authRequest);
}
/**
* 認(rèn)證成功所執(zhí)行的方法
*/
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
SysUser sysUser = new SysUser();
sysUser.setUsername(authResult.getName());
sysUser.setSysRoles(new ArrayList(authResult.getAuthorities()));
String token = JwtUtils.generateTokenExpireInMinutes(sysUser, prop.getPrivateKey(), 24 * 60);
response.addHeader("Authorization", "Bearer " + token);
ResponseUtils.write(response, HttpServletResponse.SC_OK, "用戶認(rèn)證通過(guò)!");
}
/**
* 認(rèn)證失敗所執(zhí)行的方法
*/
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
//清理上下文
SecurityContextHolder.clearContext();
log.error("AuthenticationException",failed);
//判斷異常類
if (failed instanceof InternalAuthenticationServiceException) {
ResponseUtils.write(response, HttpServletResponse.SC_FORBIDDEN, "認(rèn)證服務(wù)不正常!");
} else if (failed instanceof UsernameNotFoundException) {
ResponseUtils.write(response, HttpServletResponse.SC_FORBIDDEN, "用戶賬戶不存在!");
} else if (failed instanceof BadCredentialsException) {
ResponseUtils.write(response, HttpServletResponse.SC_FORBIDDEN, "用戶密碼是錯(cuò)的!");
} else if (failed instanceof AccountExpiredException) {
ResponseUtils.write(response, HttpServletResponse.SC_FORBIDDEN, "用戶賬戶已過(guò)期!");
} else if (failed instanceof LockedException) {
ResponseUtils.write(response, HttpServletResponse.SC_FORBIDDEN, "用戶賬戶已被鎖!");
} else if (failed instanceof CredentialsExpiredException) {
ResponseUtils.write(response, HttpServletResponse.SC_FORBIDDEN, "用戶密碼已失效!");
} else if (failed instanceof DisabledException) {
ResponseUtils.write(response, HttpServletResponse.SC_FORBIDDEN, "用戶賬戶已被鎖!");
}
}
}
9.編寫安全配置類
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private SysUserDetailsService sysUserDetailsService;
@Autowired
private RsaKeyProperties prop;
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
public AuthenticationProvider daoAuthenticationProvider() {
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
//指定認(rèn)證對(duì)象的來(lái)源
daoAuthenticationProvider.setUserDetailsService(sysUserDetailsService);
//指定密碼編碼的來(lái)源
daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
daoAuthenticationProvider.setHideUserNotFoundExceptions(false);
return daoAuthenticationProvider;
}
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(daoAuthenticationProvider());
}
@Override
public void configure(HttpSecurity http) throws Exception {
//禁用csrf保護(hù)機(jī)制
http.csrf().disable();
//禁用cors保護(hù)機(jī)制
http.cors().disable();
//禁用session會(huì)話
http.sessionManagement().disable();
//禁用form表單登錄
http.formLogin().disable();
//增加自定義認(rèn)證過(guò)濾器(認(rèn)證服務(wù)需要配置)
http.addFilter(new JwtAuthenticationFilter(super.authenticationManager(), prop));
}
}
六 訂單資源
資源服務(wù)可以有很多個(gè),這里只拿訂單服務(wù)為例,記住,資源服務(wù)中只能通過(guò)公鑰驗(yàn)證認(rèn)證
。不能簽發(fā)token
!
-
注意:本章節(jié)所有操作均在
sso-source-order
中進(jìn)行。
1.導(dǎo)入依賴
<!--web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--springSecurity-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--mybatis、mysql-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.49</version>
</dependency>
<!--引入通用子模塊-->
<dependency>
<groupId>com.oyjp</groupId>
<artifactId>sso-common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
2.編寫配置文件
server:
port: 9002
servlet:
application-display-name: sso-source-order
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/security_test2?useSSL=false
username: root
password: root
mybatis:
type-aliases-package: com.oyjp.domain
configuration:
map-underscore-to-camel-case: true
logging:
level:
com.oyjp: debug
#自定義屬性,配置公鑰路徑
rsa:
key:
publicKeyPath: E:\auth_key\rsa_key.pub
3.編寫讀取公鑰的配置類
@Data
@ConfigurationProperties(prefix = "rsa.key", ignoreInvalidFields = true)
public class RsaKeyProperties {
private String publicKeyPath;
private String privateKeyPath;
private PublicKey publicKey;
private PrivateKey privateKey;
/**
* 該方法用于初始化公鑰和私鑰的內(nèi)容
*/
@PostConstruct
public void loadRsaKey() throws Exception {
if (publicKeyPath != null) {
publicKey = RsaUtils.getPublicKey(publicKeyPath);
}
if (privateKeyPath != null) {
privateKey = RsaUtils.getPrivateKey(privateKeyPath);
}
}
}
5.編寫驗(yàn)證過(guò)濾器
/**
* 驗(yàn)證過(guò)濾器
*
* @author oyjp
*/
public class JwtVerificationFilter extends BasicAuthenticationFilter {
private RsaKeyProperties prop;
public JwtVerificationFilter(AuthenticationManager authenticationManager, RsaKeyProperties prop) {
super(authenticationManager);
this.prop = prop;
}
@Override
public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
try {
String header = request.getHeader("Authorization");
if (header == null || !header.startsWith("Bearer ")) {
//如果token的格式錯(cuò)誤,則提示用戶非法登錄
chain.doFilter(request, response);
ResponseUtils.write(response, HttpServletResponse.SC_FORBIDDEN, "用戶非法登錄!");
} else {
//如果token的格式正確,則先要獲取到token
String token = header.replace("Bearer ", "");
//使用公鑰進(jìn)行解密然后來(lái)驗(yàn)證token是否正確
Payload<SysUser> payload = JwtUtils.getInfoFromToken(token, prop.getPublicKey(), SysUser.class);
SysUser sysUser = payload.getUserInfo();
if (sysUser != null) {
UsernamePasswordAuthenticationToken authResult = new UsernamePasswordAuthenticationToken(sysUser.getUsername(), null, sysUser.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authResult);
chain.doFilter(request, response);
} else {
ResponseUtils.write(response, HttpServletResponse.SC_FORBIDDEN, "用戶驗(yàn)證失?。?);
}
}
} catch (ExpiredJwtException e) {
ResponseUtils.write(response, HttpServletResponse.SC_FORBIDDEN, "請(qǐng)您重新登錄!");
}
}
}
6.編寫安全配置類
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private RsaKeyProperties prop;
@Override
public void configure(HttpSecurity http) throws Exception {
//禁用csrf保護(hù)機(jī)制
http.csrf().disable();
//禁用cors保護(hù)機(jī)制
http.cors().disable();
//禁用session會(huì)話
http.sessionManagement().disable();
//禁用form表單登錄
http.formLogin().disable();
//增加自定義驗(yàn)證過(guò)濾器(資源服務(wù)需要配置)
http.addFilter(new JwtVerificationFilter(super.authenticationManager(), prop));
}
}
7.全局異常處理
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(AccessDeniedException.class)
public Result accessDeniedException() {
return new Result(403, "用戶權(quán)限不足!", null);
}
@ExceptionHandler(RuntimeException.class)
public Result serverException() {
return new Result(500, "服務(wù)出現(xiàn)異常!", null);
}
}
8.訂單資源控制器
@RestController
@RequestMapping("/order")
public class OrderController {
@Secured({"ROLE_ADMIN","ROLE_ORDER"})
@RequestMapping("/info")
public String info() {
return "Order Controller ...";
}
}
9.啟動(dòng)類
@SpringBootApplication
@EnableConfigurationProperties(RsaKeyProperties.class)
public class SourceOrderApplication {
public static void main(String[] args) {
SpringApplication.run(SourceOrderApplication.class, args);
}
}
七.產(chǎn)品資源
直接復(fù)制訂單服務(wù),目錄名稱改為sso-source-product
,然后修改yml配置文件、controller、啟動(dòng)類
1.修改yml配置文件
- 改一下application-display-name、port 即可
server:
port: 9003
servlet:
application-display-name: sso-source-product
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/security_test2?useSSL=false
username: root
password: root
mybatis:
type-aliases-package: com.oyjp.domain
configuration:
map-underscore-to-camel-case: true
logging:
level:
com.oyjp: debug
#自定義屬性,配置公鑰路徑
rsa:
key:
publicKeyPath: E:\auth_key\rsa_key.pub
2.產(chǎn)品資源控制器
- 編寫產(chǎn)品的controller邏輯
@RestController
@RequestMapping("/product")
public class ProductController {
@Secured({"ROLE_ADMIN", "ROLE_PRODUCT"})
@RequestMapping("/info")
public String info() {
return "Productr Controller ...";
}
}
3.啟動(dòng)類
@SpringBootApplication
@EnableConfigurationProperties(RsaKeyProperties.class)
public class SourceProductApplication {
public static void main(String[] args) {
SpringApplication.run(SourceProductApplication.class, args);
}
}
八 終極測(cè)試
1.認(rèn)證服務(wù)測(cè)試
2.訂單資源測(cè)試
3.產(chǎn)品資源測(cè)試
4.用戶狀態(tài)測(cè)試
張三
李四
王五
趙六
小七
老八
密碼錯(cuò)誤:
源碼文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-785179.html
鏈接:https://pan.baidu.com/s/1EINPwP4or0Nuj8BOEPsIyw文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-785179.html
- 提取碼:kbue
到了這里,關(guān)于【SpringBoot】集成SpringSecurity+JWT實(shí)現(xiàn)多服務(wù)單點(diǎn)登錄,原來(lái)這么easy的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!