Spring Boot + Spring Security + JWT によるシングル サインオンの実装
ソースコード
リンク: https://pan.baidu.com/s/1EINPwP4or0Nuj8BOEPsIyw
- 抽出コード:kbue
1.コンセプト
1.1.SSO
導入:
- シングル サインオン (SingleSignOn、SSO) は、ユーザーが
认证服务器
一度ログインすると、シングル サインオン システム内の他の関連システムやアプリケーション ソフトウェアにアクセスできると同時に、この実装では管理者が必要ありません。ユーザーのログイン状態を確認したり、その他の情報が変更された場合、これは複数のアプリケーション システムで行われることを意味します用户只需一次登录就可以访问所有相互信任的应用系统
。この方法は、ログインにかかる時間を削減し、ユーザー管理を容易にするため、現在一般的な分散ログイン方法です。
SSO 実装プロセス:
- したがって、分散プロジェクトでは、
每台服务器都有各自独立的session,而这些session之间是无法直接共享资源的
通常、セッションをシングル サインオンの技術ソリューションとして使用することはできません。最も合理的なシングル サインオン ソリューション プロセスは、次の図に示されています。
シングル サインオンの実装は、次の 2 つの部分に分かれています。
- ユーザー認証: クライアントが認証サーバーに対して認証要求を開始し、認証サーバーがクライアントにトークンを返します。これは主に
认证服务器
図のシステム A で行われます。认证服务器只能有一个
- ID 検証: クライアントが他のリソース サーバーにアクセスするためにトークンを運ぶとき、リソース サーバーでトークンの信頼性を検証する必要があります。これは主に図のシステム
资源服务器
B で行われます。B系统可以有很多个
1.2.JWT
JWTとは
1.3.RSA
非対称暗号化アルゴリズム
- サービスプロバイダーは 2 つのキー (公開キーと秘密キー) を生成します。秘密キーは秘密に保管され、公開キーは公開されて信頼できるクライアントに配布されます。
- 呼び出し元はプロバイダーの公開キーを取得し、それを使用してメッセージを暗号化します。
- プロバイダーは暗号化された情報を受信すると、秘密キーを使用してそれを復号化します。
RSAアルゴリズム
- これは常に最も広く使用されている「非対称暗号化アルゴリズム」です。コンピュータ ネットワークがあるところには必ず RSA アルゴリズムがあると言っても過言ではありません。このアルゴリズムは非常に信頼性が高く、キーが長いほど解読が難しくなります。公開されている文献によると、これまでに解読された最長の RSA キーは 768 バイナリ ビットです。つまり、
长度超过768位的密钥,还无法破解(至少没人公开宣布)。因此可以认为,1024位的RSA密钥基本安全,2048位的密钥极其安全。
RSA の使用プロセス:
- 秘密キーと公開キーの 2 つのキーを生成します。秘密キーは保存され、公開キーは信頼できるクライアントに発行できます。
- 秘密鍵暗号化、
持有私钥或公钥才可以解密
- 公開鍵暗号化、
持有私钥才可解密
- 秘密鍵暗号化、
- したがって、通常は認証サービスが保存され
私钥和公钥
、リソース サービスも保存されます公钥
。秘密キーは暗号化を担当し、公開キーは復号化を担当します。
2. アイデア
1.集中認証プロセスを分析する
- ユーザー認証:
UsernamePasswordAuthenticationFilter
フィルターを使用してattemptAuthentication()
認証機能を実装し、フィルターの親クラスを使用successfulAuthentication()
して認証成功後の操作を実装します。 - ID 検証:
BasicAuthenticationFilter
フィルターを使用してdoFilterInternal()
ログインしているかどうかを確認し、後続のフィルターを入力できるかどうかを判断します。
2. 分散認証プロセスを分析する
-
ユーザー認証: ほとんどの分散プロジェクトは、リクエスト本文を受信できるようにフィルター
前后端分离
を変更する必要があるアーキテクチャを備えています。また、デフォルトでは認証通過後、ユーザー情報が直接 に入れられます。UsernamePasswordAuthenticationFilter
attemptAuthentication()
successfulAuthentication()
session
- 処理方法: 変更して
successfulAuthentication()
トークンを生成し、認証通過後にユーザーに返却します。
- 処理方法: 変更して
-
本人確認:
BasicAuthenticationFilter
フィルターはdoFilterInternal()
ユーザーがログインしているかどうか、つまりユーザー情報があるかどうかsession
をチェックしていることがわかります。- 処理方法
token
:ユーザが保持するデータが正当であることを検証し、その後の認可機能が正常に利用できるようにSpringSecurity解析出用户信息
に引き渡すための検証ロジックを修正する。
- 処理方法
//Header.Payload.Signature
HMACSHA245(base64UrlEncode(header)+"."+base64UrlEncode(payload),secret)
3. プロジェクト紹介
1.親プロジェクトの紹介
今回は複数のシステムを作成する必要があるため、maven聚合工程
を使用して実装します まず親プロジェクトを作成し、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><!--认证服务子模块-->
<module>sso-source-product</module><!--产品资源服务子模块-->
<module>sso-source-order</module><!--订单资源服务子模块-->
</modules>
このプロジェクトは、認証サービス モジュール、一般モジュール、注文リソース モジュール、製品リソース モジュールの 4 つのサブモジュールで構成されています。
2. データベースをインポートする
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 '角色编号',
`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','用户权限');
INSERT INTO `sys_role`(`id`,`name`,`desc`) VALUES (2,'ROLE_ADMIN','管理权限');
INSERT INTO `sys_role`(`id`,`name`,`desc`) VALUES (3,'ROLE_PRODUCT','产品权限');
INSERT INTO `sys_role`(`id`,`name`,`desc`) VALUES (4,'ROLE_ORDER','订单权限');
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (
`id` INT(11) NOT NULL AUTO_INCREMENT COMMENT '用户编号',
`username` VARCHAR(32) NOT NULL COMMENT '用户名称',
`password` VARCHAR(128) NOT NULL COMMENT '用户密码',
`status` INT(1) NOT NULL DEFAULT '1' COMMENT '用户状态(0:关闭、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 '用户编号',
`rid` INT(11) NOT NULL COMMENT '角色编号',
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);
4つの一般的なモジュール
1. 依存関係をインポートする
<!--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>
<!--测试包-->
<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. 統一フォーマット
2.1. 統合ロードオブジェクト
/**
* 为了方便后期获取token中的用户信息,将token中载荷部分单独封装成一个对象
* @author JianpengOuYang
*/
@Data
public class Payload<T> implements Serializable {
private String id;
private T userInfo;
private Date expiration;
}
2.2. 結果を一律に返す
/**
* 统一处理返回结果
* @author JianpengOuYang
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result implements Serializable {
private Integer code;
private String msg;
private Object data;
}
3. よく使われるツール
3.1.Jsonツールクラス
/**
* 对Jackson中的方法进行了简单封装
* @author JianpengOuYang
*/
public class JsonUtils {
private static final Logger logger = LoggerFactory.getLogger(JsonUtils.class);
private static final ObjectMapper mapper = new ObjectMapper();
/**
* 将指定对象序列化为一个json字符串
*
* @param obj 指定对象
* @return 返回一个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序列化出错:" + obj, e);
return null;
}
}
/**
* 将指定json字符串解析为指定类型对象
*
* @param json json字符串
* @param tClass 指定类型
* @return 返回一个指定类型对象
*/
public static <T> T toBean(String json, Class<T> tClass) {
try {
return mapper.readValue(json, tClass);
} catch (IOException e) {
logger.error("json解析出错:" + json, e);
return null;
}
}
/**
* 将指定输入流解析为指定类型对象
*
* @param inputStream 输入流对象
* @param tClass 指定类型
* @return 返回一个指定类型对象
*/
public static <T> T toBean(InputStream inputStream, Class<T> tClass) {
try {
return mapper.readValue(inputStream, tClass);
} catch (IOException e) {
logger.error("json解析出错:" + inputStream, e);
return null;
}
}
/**
* 将指定json字符串解析为指定类型集合
*
* @param json json字符串
* @param eClass 指定元素类型
* @return 返回一个指定类型集合
*/
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解析出错:" + json, e);
return null;
}
}
/**
* 将指定json字符串解析为指定键值对类型集合
*
* @param json json字符串
* @param kClass 指定键类型
* @param vClass 指定值类型
* @return 返回一个指定键值对类型集合
*/
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解析出错:" + json, e);
return null;
}
}
/**
* 将指定json字符串解析为一个复杂类型对象
*
* @param json json字符串
* @param type 复杂类型
* @return 返回一个复杂类型对象
*/
public static <T> T nativeRead(String json, TypeReference<T> type) {
try {
return mapper.readValue(json, type);
} catch (IOException e) {
logger.error("json解析出错:" + json, e);
return null;
}
}
}
3.2.Jwtツールクラス
/**
* 生成token以及校验token相关方法
*
* @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 载荷中的数据
* @param privateKey 私钥
* @param expire 过期时间,单位分钟
* @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())//过期时间
.signWith(privateKey, SignatureAlgorithm.RS256)//Signature,使用privateKey作为密钥
.compact();
}
/**
* 私钥加密token
*
* @param userInfo 载荷中的数据
* @param privateKey 私钥
* @param expire 过期时间,单位秒
* @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 用户请求中的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 用户请求中的令牌
* @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());//获取过期时间
return claims;
}
/**
* 获取token中的载荷信息
*
* @param token 用户请求中的令牌
* @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ツールクラス
/**
* 对Rsa操作进行了简单封装
*
* @author JianpengOuYang
*/
public class RsaUtils {
private static final int DEFAULT_KEY_SIZE = 2048;
/**
* 从文件中读取公钥
*
* @param filename 公钥保存路径,相对于classpath
* @return 公钥对象
* @throws Exception
*/
public static PublicKey getPublicKey(String filename) throws Exception {
byte[] bytes = readFile(filename);
return getPublicKey(bytes);
}
/**
* 从文件中读取密钥
*
* @param filename 私钥保存路径,相对于classpath
* @return 私钥对象
* @throws Exception
*/
public static PrivateKey getPrivateKey(String filename) throws Exception {
byte[] bytes = readFile(filename);
return getPrivateKey(bytes);
}
/**
* 获取公钥
*
* @param bytes 公钥的字节形式
* @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 私钥的字节形式
* @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);
}
/**
* 根据密文,生成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. 応答/要求ツールクラス
/**
* 请求工具类
*
* @author oyjp
*/
public class RequestUtils {
private static final Logger logger = LoggerFactory.getLogger(RequestUtils.class);
/**
* 从请求对象的输入流中获取指定类型对象
*
* @param request 请求对象
* @param clazz 指定类型
* @return 指定类型对象
*/
public static <T> T read(HttpServletRequest request, Class<T> clazz) {
try {
return JsonUtils.toBean(request.getInputStream(), clazz);
} catch (Exception e) {
logger.error("读取出错:" + clazz, e);
return null;
}
}
}
/**
* 响应工具类
*
* @author oyjp
*/
public class ResponseUtils {
private static final Logger logger = LoggerFactory.getLogger(ResponseUtils.class);
/**
* 向浏览器响应一个json字符串
*
* @param response 响应对象
* @param status 状态码
* @param msg 响应信息
*/
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("响应出错:" + 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);
}
}
5つの認証サービス
注: この章のすべての操作
sso-auth-server
は で実行されます。
1. 依存関係をインポートする
<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. 設定ファイルの作成
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;
/**
* 该方法用于初始化公钥和私钥的内容
*/
@PostConstruct
public void loadRsaKey() throws Exception {
if (publicKeyPath != null) {
publicKey = RsaUtils.getPublicKey(publicKeyPath);
}
if (privateKeyPath != null) {
privateKey = RsaUtils.getPrivateKey(privateKeyPath);
}
}
}
4.スタートアップクラスを書く
@SpringBootApplication
@EnableConfigurationProperties(RsaKeyProperties.class) //启动时加载配置类
public class AuthServerApplication {
public static void main(String[] args) {
SpringApplication.run(AuthServerApplication.class, args);
}
}
5. エンティティクラスを作成する
ユーザークラスの実装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;
}
/**
* 是否账号已过期
*/
@JsonIgnore
@Override
public boolean isAccountNonExpired() {
return status != 1;
}
/**
* 是否账号已被锁
*/
@JsonIgnore
@Override
public boolean isAccountNonLocked() {
return status != 2;
}
/**
* 是否凭证已过期
*/
@JsonIgnore
@Override
public boolean isCredentialsNonExpired() {
return status != 3;
}
/**
* 是否账号已禁用
*/
@JsonIgnore
@Override
public boolean isEnabled() {
return status != 4;
}
}
ロールクラスの実装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 {
//根据用户名称查询所对应的用户信息
@Select("select * from `sys_user` where `username` = #{username}")
@Results({
//主键字段映射,property代表Java对象属性,column代表数据库字段
@Result(property = "id", column = "id", id = true),
//普通字段映射,property代表Java对象属性,column代表数据库字段
@Result(property = "username", column = "username"),
@Result(property = "password", column = "password"),
@Result(property = "status", column = "status"),
//角色列表映射,根据用户id查询该用户所对应的角色列表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 {
//根据用户编号查询角色列表
@Select("select * from `sys_role` where id in (" +
" select rid from `sys_user_role` where uid = #{uid}" +
")")
List<SysRole> findByUid(Integer uid);
}
7. 書き込みサービスインターフェイス
springSecurityとreloadUserByUsername()のUserDetailsServiceインターフェースを実装します。
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 {
//根据用户名去数据库中查询指定用户,这就要保证数据库中的用户的名称必须唯一,否则将会报错
SysUser sysUser = sysUserMapper.findByUsername(username);
//如果没有查询到这个用户,说明数据库中不存在此用户,认证失败,此时需要抛出用户账户不存在
if (sysUser == null) {
throw new UsernameNotFoundException("user not exist.");
}
return sysUser;
}
}
8.認証フィルターの作成
/**
* 认证过滤器
*
*/
@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);
}
/**
* 认证成功所执行的方法
*/
@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, "用户认证通过!");
}
/**
* 认证失败所执行的方法
*/
@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, "认证服务不正常!");
} else if (failed instanceof UsernameNotFoundException) {
ResponseUtils.write(response, HttpServletResponse.SC_FORBIDDEN, "用户账户不存在!");
} else if (failed instanceof BadCredentialsException) {
ResponseUtils.write(response, HttpServletResponse.SC_FORBIDDEN, "用户密码是错的!");
} else if (failed instanceof AccountExpiredException) {
ResponseUtils.write(response, HttpServletResponse.SC_FORBIDDEN, "用户账户已过期!");
} 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();
//指定认证对象的来源
daoAuthenticationProvider.setUserDetailsService(sysUserDetailsService);
//指定密码编码的来源
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保护机制
http.csrf().disable();
//禁用cors保护机制
http.cors().disable();
//禁用session会话
http.sessionManagement().disable();
//禁用form表单登录
http.formLogin().disable();
//增加自定义认证过滤器(认证服务需要配置)
http.addFilter(new JwtAuthenticationFilter(super.authenticationManager(), prop));
}
}
6 つのオーダー リソース
多くのリソース サービスが存在する可能性があります。ここでは、順序サービスのみを例として取り上げます。リソース サービスは渡すことしかできないことに注意してください公钥验证认证
。いや签发token
!
-
注: この章のすべての操作
sso-source-order
は で実行されます。
1. 依存関係をインポートする
<!--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;
/**
* 该方法用于初始化公钥和私钥的内容
*/
@PostConstruct
public void loadRsaKey() throws Exception {
if (publicKeyPath != null) {
publicKey = RsaUtils.getPublicKey(publicKeyPath);
}
if (privateKeyPath != null) {
privateKey = RsaUtils.getPrivateKey(privateKeyPath);
}
}
}
5. 検証フィルターを作成する
/**
* 验证过滤器
*
* @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的格式错误,则提示用户非法登录
chain.doFilter(request, response);
ResponseUtils.write(response, HttpServletResponse.SC_FORBIDDEN, "用户非法登录!");
} else {
//如果token的格式正确,则先要获取到token
String token = header.replace("Bearer ", "");
//使用公钥进行解密然后来验证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, "用户验证失败!");
}
}
} catch (ExpiredJwtException e) {
ResponseUtils.write(response, HttpServletResponse.SC_FORBIDDEN, "请您重新登录!");
}
}
}
6. セキュリティ構成クラスを作成する
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private RsaKeyProperties prop;
@Override
public void configure(HttpSecurity http) throws Exception {
//禁用csrf保护机制
http.csrf().disable();
//禁用cors保护机制
http.cors().disable();
//禁用session会话
http.sessionManagement().disable();
//禁用form表单登录
http.formLogin().disable();
//增加自定义验证过滤器(资源服务需要配置)
http.addFilter(new JwtVerificationFilter(super.authenticationManager(), prop));
}
}
7. グローバル例外処理
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(AccessDeniedException.class)
public Result accessDeniedException() {
return new Result(403, "用户权限不足!", null);
}
@ExceptionHandler(RuntimeException.class)
public Result serverException() {
return new Result(500, "服务出现异常!", null);
}
}
8. リソースコントローラーの順序付け
@RestController
@RequestMapping("/order")
public class OrderController {
@Secured({
"ROLE_ADMIN","ROLE_ORDER"})
@RequestMapping("/info")
public String info() {
return "Order Controller ...";
}
}
9. スタートアップクラス
@SpringBootApplication
@EnableConfigurationProperties(RsaKeyProperties.class)
public class SourceOrderApplication {
public static void main(String[] args) {
SpringApplication.run(SourceOrderApplication.class, args);
}
}
7. 製品リソース
order サービスを直接コピーし、ディレクトリ名を に変更してsso-source-product
から、yml 構成ファイル、コントローラー、およびスタートアップ クラスを変更します。
1.yml設定ファイルを変更します。
- application-display-name とポートを変更するだけです。
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. 製品リソースコントローラー
- 製品のコントローラーロジックを記述します
@RestController
@RequestMapping("/product")
public class ProductController {
@Secured({
"ROLE_ADMIN", "ROLE_PRODUCT"})
@RequestMapping("/info")
public String info() {
return "Productr Controller ...";
}
}
3. スタートアップクラス
@SpringBootApplication
@EnableConfigurationProperties(RsaKeyProperties.class)
public class SourceProductApplication {
public static void main(String[] args) {
SpringApplication.run(SourceProductApplication.class, args);
}
}
8つの究極のテスト
1. 認証サービスのテスト
2. リソースのテストを注文する
3. 製品リソースのテスト
4. ユーザーステータステスト
張三
ジョン・ドウ
ワン・ウー
趙劉
シャオチー
ラオバ
間違ったパスワード: