Spring Boot+Spring Security+JWT实现单点登录
一.概念
1.1.SSO
介绍:
- 单点登录(SingleSignOn,SSO),当用户在身份
认证服务器
上登录一次以后,即可获得访问单点登录系统中其他关联系统和应用软件的权限,同时这种实现是不需要管理员对用户的登录状态或其他信息进行修改的,这意味着在多个应用系统中,用户只需一次登录就可以访问所有相互信任的应用系统
。这种方式减少了由登录产生的时间消耗,辅助了用户管理,是目前比较流行的一种分布式登录方式。
SSO实现流程:
- 在分布式项目中,
每台服务器都有各自独立的session,而这些session之间是无法直接共享资源的
,所以,session通常不能被作为单点登录的技术方案。最合理的单点登录方案流程如下图所示:
单点登录的实现分2部分:
- 用户认证:客户端向认证服务器发起认证请求,认证服务器给客户端返回令牌token, 主要在
认证服务器
中完成,即图中的A系统,注意认证服务器只能有一个
- 身份校验: 客户端携带token去访问其他资源服务器时,在资源服务器中要对token的真伪进行检验,主要在
资源服务器
中完成,即图中的B系统,这里B系统可以有很多个
1.2.JWT
什么是JWT
- 【JavaWeb】关于JWT做认证授权的十万个理由(JSON Web Token)
1.3.RSA
非对称加密算法
- 服务提供方生成两把密钥(公钥和私钥)。私钥隐秘保存,公钥公开,下发给信任客户端
- 调用方获取提供方的公钥,然后用它对信息加密。
- 提供方接收到调用加密后的信息后,用私钥解密。
RSA算法
- 一直是最广为使用的"非对称加密算法"。毫不夸张地说,只要有计算机网络的地方,就有RSA算法。这种算法非常可靠,密钥越长,它就越难破解。根据已经披露的文献,目前被破解的最长RSA密钥是768个二进制位。也就是说,
长度超过768位的密钥,还无法破解(至少没人公开宣布)。因此可以认为,1024位的RSA密钥基本安全,2048位的密钥极其安全。
RSA使用流程:
- 生成两把密钥:私钥和公钥,私钥保存起来,公钥可以下发给信任客户端- 私钥加密,
持有私钥或公钥才可以解密
- 公钥加密,持有私钥才可解密
- 因此,认证服务一般存放
私钥和公钥
,而资源服务一般存放公钥
。私钥负责加密,公钥负责解密。
二.思路
1.分析集中式认证流程
- 用户认证:使用
UsernamePasswordAuthenticationFilter
过滤器中attemptAuthentication()
实现认证功能,该过滤器父类中successfulAuthentication()
实现认证成功后的操作。 - 身份校验:使用
BasicAuthenticationFilter
过滤器中doFilterInternal()
验证是否登录,以决定能否进入后续过滤器。
2.分析分布式认证流程
- 用户认证:分布式项目多数是
前后端分离
的架构,需要修改UsernamePasswordAuthenticationFilter
过滤器中attemptAuthentication()
,让其能够接收请求体。另外,默认successfulAuthentication()
在认证通过后,是把用户信息直接放入session
就完事了- 处理方式:修改successfulAuthentication()
,在认证通过后生成token并返回给用户。 - 身份校验: 原来
BasicAuthenticationFilter
过滤器中doFilterInternal()
校验用户是否登录,就是看session
中是否有用户信息- 处理方式:校验逻辑修改为,验证用户携带的token
合法,并解析出用户信息
,交给SpringSecurity,以便于后续的授权功能可以正常使用。
//Header.Payload.SignatureHMACSHA245(base64UrlEncode(header)+"."+base64UrlEncode(payload),secret)
三.工程介绍
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>
该工程由四个子模块组成,一个认证服务模块,一个通用模块,一个订单资源模块,一个产品资源模块
2.导入数据库
DROPDATABASEIFEXISTS`security_test2`;CREATEDATABASE`security_test2`;USE`security_test2`;DROPTABLEIFEXISTS`sys_role`;CREATETABLE`sys_role`(`id`INT(11)NOTNULLAUTO_INCREMENTCOMMENT'角色编号',`name`VARCHAR(32)NOTNULLCOMMENT'角色名称',`desc`VARCHAR(32)NOTNULLCOMMENT'角色描述',PRIMARYKEY(`id`))ENGINE=INNODBAUTO_INCREMENT=5DEFAULTCHARSET=utf8;INSERTINTO`sys_role`(`id`,`name`,`desc`)VALUES(1,'ROLE_USER','用户权限');INSERTINTO`sys_role`(`id`,`name`,`desc`)VALUES(2,'ROLE_ADMIN','管理权限');INSERTINTO`sys_role`(`id`,`name`,`desc`)VALUES(3,'ROLE_PRODUCT','产品权限');INSERTINTO`sys_role`(`id`,`name`,`desc`)VALUES(4,'ROLE_ORDER','订单权限');DROPTABLEIFEXISTS`sys_user`;CREATETABLE`sys_user`(`id`INT(11)NOTNULLAUTO_INCREMENTCOMMENT'用户编号',`username`VARCHAR(32)NOTNULLCOMMENT'用户名称',`password`VARCHAR(128)NOTNULLCOMMENT'用户密码',`status`INT(1)NOTNULLDEFAULT'1'COMMENT'用户状态(0:关闭、1:开启)',PRIMARYKEY(`id`))ENGINE=INNODBAUTO_INCREMENT=4DEFAULTCHARSET=utf8;INSERTINTO`sys_user`(`id`,`username`,`password`,`status`)VALUES(1,'zhangsan','$2a$10$M7fmKpMZEkkzrTBiKie.EeAKZhQDrWAltpCA1y/py5AU/8lyiNB8y',0);INSERTINTO`sys_user`(`id`,`username`,`password`,`status`)VALUES(2,'lisi','$2a$10$M7fmKpMZEkkzrTBiKie.EeAKZhQDrWAltpCA1y/py5AU/8lyiNB8y',1);INSERTINTO`sys_user`(`id`,`username`,`password`,`status`)VALUES(3,'wangwu','$2a$10$M7fmKpMZEkkzrTBiKie.EeAKZhQDrWAltpCA1y/py5AU/8lyiNB8y',2);INSERTINTO`sys_user`(`id`,`username`,`password`,`status`)VALUES(4,'zhaoliu','$2a$10$M7fmKpMZEkkzrTBiKie.EeAKZhQDrWAltpCA1y/py5AU/8lyiNB8y',3);INSERTINTO`sys_user`(`id`,`username`,`password`,`status`)VALUES(5,'xiaoqi','$2a$10$M7fmKpMZEkkzrTBiKie.EeAKZhQDrWAltpCA1y/py5AU/8lyiNB8y',4);DROPTABLEIFEXISTS`sys_user_role`;CREATETABLE`sys_user_role`(`uid`INT(11)NOTNULLCOMMENT'用户编号',`rid`INT(11)NOTNULLCOMMENT'角色编号',PRIMARYKEY(`uid`,`rid`))ENGINE=INNODBDEFAULTCHARSET=utf8;INSERTINTO`sys_user_role`(`uid`,`rid`)VALUES(1,1);INSERTINTO`sys_user_role`(`uid`,`rid`)VALUES(1,3);INSERTINTO`sys_user_role`(`uid`,`rid`)VALUES(2,1);INSERTINTO`sys_user_role`(`uid`,`rid`)VALUES(2,4);INSERTINTO`sys_user_role`(`uid`,`rid`)VALUES(3,1);INSERTINTO`sys_user_role`(`uid`,`rid`)VALUES(3,2);INSERTINTO`sys_user_role`(`uid`,`rid`)VALUES(3,3);INSERTINTO`sys_user_role`(`uid`,`rid`)VALUES(3,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
*/@DatapublicclassPayload<T>implementsSerializable{privateString id;privateT userInfo;privateDate expiration;}
2.2.统一返回结果
/**
* 统一处理返回结果
* @author JianpengOuYang
*/@Data@NoArgsConstructor@AllArgsConstructorpublicclassResultimplementsSerializable{privateInteger code;privateString msg;privateObject data;}
3.常用工具
3.1.Json工具类
/**
* 对Jackson中的方法进行了简单封装
* @author JianpengOuYang
*/publicclassJsonUtils{privatestaticfinalLogger logger =LoggerFactory.getLogger(JsonUtils.class);privatestaticfinalObjectMapper mapper =newObjectMapper();/**
* 将指定对象序列化为一个json字符串
*
* @param obj 指定对象
* @return 返回一个json字符串
*/publicstaticStringtoString(Object obj){if(obj ==null){returnnull;}if(obj.getClass()==String.class){return(String) obj;}try{return mapper.writeValueAsString(obj);}catch(JsonProcessingException e){
logger.error("json序列化出错:"+ obj, e);returnnull;}}/**
* 将指定json字符串解析为指定类型对象
*
* @param json json字符串
* @param tClass 指定类型
* @return 返回一个指定类型对象
*/publicstatic<T>TtoBean(String json,Class<T> tClass){try{return mapper.readValue(json, tClass);}catch(IOException e){
logger.error("json解析出错:"+ json, e);returnnull;}}/**
* 将指定输入流解析为指定类型对象
*
* @param inputStream 输入流对象
* @param tClass 指定类型
* @return 返回一个指定类型对象
*/publicstatic<T>TtoBean(InputStream inputStream,Class<T> tClass){try{return mapper.readValue(inputStream, tClass);}catch(IOException e){
logger.error("json解析出错:"+ inputStream, e);returnnull;}}/**
* 将指定json字符串解析为指定类型集合
*
* @param json json字符串
* @param eClass 指定元素类型
* @return 返回一个指定类型集合
*/publicstatic<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);returnnull;}}/**
* 将指定json字符串解析为指定键值对类型集合
*
* @param json json字符串
* @param kClass 指定键类型
* @param vClass 指定值类型
* @return 返回一个指定键值对类型集合
*/publicstatic<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);returnnull;}}/**
* 将指定json字符串解析为一个复杂类型对象
*
* @param json json字符串
* @param type 复杂类型
* @return 返回一个复杂类型对象
*/publicstatic<T>TnativeRead(String json,TypeReference<T> type){try{return mapper.readValue(json, type);}catch(IOException e){
logger.error("json解析出错:"+ json, e);returnnull;}}}
3.2.Jwt工具类
/**
* 生成token以及校验token相关方法
*
* @author JianpengOuYang
*/publicclassJwtUtils{privatestaticfinalStringJWT_PAYLOAD_USER_KEY="user";privatestaticStringcreateJTI(){returnnewString(Base64.getEncoder().encode(UUID.randomUUID().toString().getBytes()));}/**
* 私钥加密token
*
* @param userInfo 载荷中的数据
* @param privateKey 私钥
* @param expire 过期时间,单位分钟
* @return JWT
*/publicstaticStringgenerateTokenExpireInMinutes(Object userInfo,PrivateKey privateKey,int expire){returnJwts.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
*/publicstaticStringgenerateTokenExpireInSeconds(Object userInfo,PrivateKey privateKey,int expire){returnJwts.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>
*/privatestaticJws<Claims>parserToken(String token,PublicKey publicKey){returnJwts.parserBuilder().setSigningKey(publicKey).build().parseClaimsJws(token);}/**
* 获取token中的用户信息
*
* @param token 用户请求中的令牌
* @param publicKey 公钥
* @return 用户信息
*/publicstatic<T>Payload<T>getInfoFromToken(String token,PublicKey publicKey,Class<T> userType){Jws<Claims> claimsJws =parserToken(token, publicKey);Claims body = claimsJws.getBody();Payload<T> claims =newPayload<>();
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 用户信息
*/publicstatic<T>Payload<T>getInfoFromToken(String token,PublicKey publicKey){Jws<Claims> claimsJws =parserToken(token, publicKey);Claims body = claimsJws.getBody();Payload<T> claims =newPayload<>();
claims.setId(body.getId());
claims.setExpiration(body.getExpiration());return claims;}}
3.3.Rsa工具类
/**
* 对Rsa操作进行了简单封装
*
* @author JianpengOuYang
*/publicclassRsaUtils{privatestaticfinalintDEFAULT_KEY_SIZE=2048;/**
* 从文件中读取公钥
*
* @param filename 公钥保存路径,相对于classpath
* @return 公钥对象
* @throws Exception
*/publicstaticPublicKeygetPublicKey(String filename)throwsException{byte[] bytes =readFile(filename);returngetPublicKey(bytes);}/**
* 从文件中读取密钥
*
* @param filename 私钥保存路径,相对于classpath
* @return 私钥对象
* @throws Exception
*/publicstaticPrivateKeygetPrivateKey(String filename)throwsException{byte[] bytes =readFile(filename);returngetPrivateKey(bytes);}/**
* 获取公钥
*
* @param bytes 公钥的字节形式
* @return
* @throws Exception
*/privatestaticPublicKeygetPublicKey(byte[] bytes)throwsException{
bytes =Base64.getDecoder().decode(bytes);X509EncodedKeySpec spec =newX509EncodedKeySpec(bytes);KeyFactory factory =KeyFactory.getInstance("RSA");return factory.generatePublic(spec);}/**
* 获取密钥
*
* @param bytes 私钥的字节形式
* @return
* @throws Exception
*/privatestaticPrivateKeygetPrivateKey(byte[] bytes)throwsNoSuchAlgorithmException,InvalidKeySpecException{
bytes =Base64.getDecoder().decode(bytes);PKCS8EncodedKeySpec spec =newPKCS8EncodedKeySpec(bytes);KeyFactory factory =KeyFactory.getInstance("RSA");return factory.generatePrivate(spec);}/**
* 根据密文,生成rsa公钥和私钥,并写入指定文件
*
* @param publicKeyFilename 公钥文件路径
* @param privateKeyFilename 私钥文件路径
* @param secret 生成密钥的密文
*/publicstaticvoidgenerateKey(String publicKeyFilename,String privateKeyFilename,String secret,int keySize)throwsException{KeyPairGenerator keyPairGenerator =KeyPairGenerator.getInstance("RSA");SecureRandom secureRandom =newSecureRandom(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);}privatestaticbyte[]readFile(String fileName)throwsException{returnFiles.readAllBytes(newFile(fileName).toPath());}privatestaticvoidwriteFile(String destPath,byte[] bytes)throwsIOException{File dest =newFile(destPath);File parentFile = dest.getParentFile();if(!parentFile.exists()){
parentFile.mkdirs();}if(!dest.exists()){
dest.createNewFile();}Files.write(dest.toPath(), bytes);}}
3.4.Response/Request工具类
/**
* 请求工具类
*
* @author oyjp
*/publicclassRequestUtils{privatestaticfinalLogger logger =LoggerFactory.getLogger(RequestUtils.class);/**
* 从请求对象的输入流中获取指定类型对象
*
* @param request 请求对象
* @param clazz 指定类型
* @return 指定类型对象
*/publicstatic<T>Tread(HttpServletRequest request,Class<T> clazz){try{returnJsonUtils.toBean(request.getInputStream(), clazz);}catch(Exception e){
logger.error("读取出错:"+ clazz, e);returnnull;}}}
/**
* 响应工具类
*
* @author oyjp
*/publicclassResponseUtils{privatestaticfinalLogger logger =LoggerFactory.getLogger(ResponseUtils.class);/**
* 向浏览器响应一个json字符串
*
* @param response 响应对象
* @param status 状态码
* @param msg 响应信息
*/publicstaticvoidwrite(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(newResult(status, msg,null)).getBytes();OutputStream out = response.getOutputStream();
out.write(bytes);}catch(Exception e){
logger.error("响应出错:"+ msg, e);}}}
4.生成密钥
- 使用密钥在指定位置生成公钥/私钥文件
publicclassRsaUtilsTest{privateString publicFile ="E:\\auth_key\\rsa_key.pub";privateString privateFile ="E:\\auth_key\\rsa_key";privateString secret ="JianpengOuYangSecret";@TestpublicvoidgenerateKey()throwsException{RsaUtils.generateKey(publicFile, privateFile, secret,2048);}}
五 认证服务
注意:本章节所有操作均在
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:9001servlet: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:truelogging:level:com.oyjp: debug
#自定义属性,配置私钥路径rsa:key:privateKeyPath: E:\auth_key\rsa_key
3.编写读取公钥的配置类
@Data@ConfigurationProperties(prefix ="rsa.key", ignoreInvalidFields =true)publicclassRsaKeyProperties{privateString publicKeyPath;privateString privateKeyPath;privatePublicKey publicKey;privatePrivateKey privateKey;/**
* 该方法用于初始化公钥和私钥的内容
*/@PostConstructpublicvoidloadRsaKey()throwsException{if(publicKeyPath !=null){
publicKey =RsaUtils.getPublicKey(publicKeyPath);}if(privateKeyPath !=null){
privateKey =RsaUtils.getPrivateKey(privateKeyPath);}}}
4.编写启动类
@SpringBootApplication@EnableConfigurationProperties(RsaKeyProperties.class)//启动时加载配置类publicclassAuthServerApplication{publicstaticvoidmain(String[] args){SpringApplication.run(AuthServerApplication.class, args);}}
5.编写实体类
**用户类实现
springSecurity的UserDetails 接口
**
@DatapublicclassSysUserimplementsUserDetails{privateInteger id;privateString username;privateString password;privateInteger status;privateList<SysRole> sysRoles;@JsonIgnore@OverridepublicCollection<?extendsGrantedAuthority>getAuthorities(){return sysRoles;}/**
* 是否账号已过期
*/@JsonIgnore@OverridepublicbooleanisAccountNonExpired(){return status !=1;}/**
* 是否账号已被锁
*/@JsonIgnore@OverridepublicbooleanisAccountNonLocked(){return status !=2;}/**
* 是否凭证已过期
*/@JsonIgnore@OverridepublicbooleanisCredentialsNonExpired(){return status !=3;}/**
* 是否账号已禁用
*/@JsonIgnore@OverridepublicbooleanisEnabled(){return status !=4;}}
**角色类实现
springSecurity的GrantedAuthority接口
**
@DatapublicclassSysRoleimplementsGrantedAuthority{privateInteger id;privateString name;privateString desc;@JsonIgnore@OverridepublicStringgetAuthority(){return name;}}
6.编写映射接口
查用户信息
@MapperpublicinterfaceSysUserMapper{//根据用户名称查询所对应的用户信息@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"))})SysUserfindByUsername(String username);}
查角色信息
@MapperpublicinterfaceSysRoleMapper{//根据用户编号查询角色列表@Select("select * from `sys_role` where id in ("+" select rid from `sys_user_role` where uid = #{uid}"+")")List<SysRole>findByUid(Integer uid);}
7.编写服务接口
实现springSecurity的UserDetailsService 接口,重新loadUserByUsername()
publicinterfaceSysUserDetailsServiceextendsUserDetailsService{}
@Service@TransactionalpublicclassSysUserDetailsServiceImplimplementsSysUserDetailsService{@Autowired(required =false)privateSysUserMapper sysUserMapper;@OverridepublicUserDetailsloadUserByUsername(String username)throwsUsernameNotFoundException{//根据用户名去数据库中查询指定用户,这就要保证数据库中的用户的名称必须唯一,否则将会报错SysUser sysUser = sysUserMapper.findByUsername(username);//如果没有查询到这个用户,说明数据库中不存在此用户,认证失败,此时需要抛出用户账户不存在if(sysUser ==null){thrownewUsernameNotFoundException("user not exist.");}return sysUser;}}
8.编写认证过滤器
/**
* 认证过滤器
*
*/@Slf4jpublicclassJwtAuthenticationFilterextendsUsernamePasswordAuthenticationFilter{privateAuthenticationManager authenticationManager;privateRsaKeyProperties prop;publicJwtAuthenticationFilter(AuthenticationManager authenticationManager,RsaKeyProperties prop){this.authenticationManager = authenticationManager;this.prop = prop;}@OverridepublicAuthenticationattemptAuthentication(HttpServletRequest request,HttpServletResponse response)throwsAuthenticationException{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 =newUsernamePasswordAuthenticationToken(username, password);return authenticationManager.authenticate(authRequest);}/**
* 认证成功所执行的方法
*/@OverrideprotectedvoidsuccessfulAuthentication(HttpServletRequest request,HttpServletResponse response,FilterChain chain,Authentication authResult)throwsIOException,ServletException{SysUser sysUser =newSysUser();
sysUser.setUsername(authResult.getName());
sysUser.setSysRoles(newArrayList(authResult.getAuthorities()));String token =JwtUtils.generateTokenExpireInMinutes(sysUser, prop.getPrivateKey(),24*60);
response.addHeader("Authorization","Bearer "+ token);ResponseUtils.write(response,HttpServletResponse.SC_OK,"用户认证通过!");}/**
* 认证失败所执行的方法
*/@OverrideprotectedvoidunsuccessfulAuthentication(HttpServletRequest request,HttpServletResponse response,AuthenticationException failed)throwsIOException,ServletException{//清理上下文SecurityContextHolder.clearContext();
log.error("AuthenticationException",failed);//判断异常类if(failed instanceofInternalAuthenticationServiceException){ResponseUtils.write(response,HttpServletResponse.SC_FORBIDDEN,"认证服务不正常!");}elseif(failed instanceofUsernameNotFoundException){ResponseUtils.write(response,HttpServletResponse.SC_FORBIDDEN,"用户账户不存在!");}elseif(failed instanceofBadCredentialsException){ResponseUtils.write(response,HttpServletResponse.SC_FORBIDDEN,"用户密码是错的!");}elseif(failed instanceofAccountExpiredException){ResponseUtils.write(response,HttpServletResponse.SC_FORBIDDEN,"用户账户已过期!");}elseif(failed instanceofLockedException){ResponseUtils.write(response,HttpServletResponse.SC_FORBIDDEN,"用户账户已被锁!");}elseif(failed instanceofCredentialsExpiredException){ResponseUtils.write(response,HttpServletResponse.SC_FORBIDDEN,"用户密码已失效!");}elseif(failed instanceofDisabledException){ResponseUtils.write(response,HttpServletResponse.SC_FORBIDDEN,"用户账户已被锁!");}}}
9.编写安全配置类
@Configuration@EnableWebSecurity@EnableGlobalMethodSecurity(securedEnabled =true)publicclassWebSecurityConfigextendsWebSecurityConfigurerAdapter{@AutowiredprivateSysUserDetailsService sysUserDetailsService;@AutowiredprivateRsaKeyProperties prop;@BeanpublicBCryptPasswordEncoderpasswordEncoder(){returnnewBCryptPasswordEncoder();}publicAuthenticationProviderdaoAuthenticationProvider(){DaoAuthenticationProvider daoAuthenticationProvider =newDaoAuthenticationProvider();//指定认证对象的来源
daoAuthenticationProvider.setUserDetailsService(sysUserDetailsService);//指定密码编码的来源
daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
daoAuthenticationProvider.setHideUserNotFoundExceptions(false);return daoAuthenticationProvider;}@Overridepublicvoidconfigure(AuthenticationManagerBuilder auth)throwsException{
auth.authenticationProvider(daoAuthenticationProvider());}@Overridepublicvoidconfigure(HttpSecurity http)throwsException{//禁用csrf保护机制
http.csrf().disable();//禁用cors保护机制
http.cors().disable();//禁用session会话
http.sessionManagement().disable();//禁用form表单登录
http.formLogin().disable();//增加自定义认证过滤器(认证服务需要配置)
http.addFilter(newJwtAuthenticationFilter(super.authenticationManager(), prop));}}
六 订单资源
**资源服务可以有很多个,这里只拿订单服务为例,记住,资源服务中只能通过
公钥验证认证
。不能
签发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:9002servlet: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:truelogging:level:com.oyjp: debug
#自定义属性,配置公钥路径rsa:key:publicKeyPath: E:\auth_key\rsa_key.pub
3.编写读取公钥的配置类
@Data@ConfigurationProperties(prefix ="rsa.key", ignoreInvalidFields =true)publicclassRsaKeyProperties{privateString publicKeyPath;privateString privateKeyPath;privatePublicKey publicKey;privatePrivateKey privateKey;/**
* 该方法用于初始化公钥和私钥的内容
*/@PostConstructpublicvoidloadRsaKey()throwsException{if(publicKeyPath !=null){
publicKey =RsaUtils.getPublicKey(publicKeyPath);}if(privateKeyPath !=null){
privateKey =RsaUtils.getPrivateKey(privateKeyPath);}}}
5.编写验证过滤器
/**
* 验证过滤器
*
* @author oyjp
*/publicclassJwtVerificationFilterextendsBasicAuthenticationFilter{privateRsaKeyProperties prop;publicJwtVerificationFilter(AuthenticationManager authenticationManager,RsaKeyProperties prop){super(authenticationManager);this.prop = prop;}@OverridepublicvoiddoFilterInternal(HttpServletRequest request,HttpServletResponse response,FilterChain chain)throwsIOException,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的格式正确,则先要获取到tokenString token = header.replace("Bearer ","");//使用公钥进行解密然后来验证token是否正确Payload<SysUser> payload =JwtUtils.getInfoFromToken(token, prop.getPublicKey(),SysUser.class);SysUser sysUser = payload.getUserInfo();if(sysUser !=null){UsernamePasswordAuthenticationToken authResult =newUsernamePasswordAuthenticationToken(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)publicclassWebSecurityConfigextendsWebSecurityConfigurerAdapter{@AutowiredprivateRsaKeyProperties prop;@Overridepublicvoidconfigure(HttpSecurity http)throwsException{//禁用csrf保护机制
http.csrf().disable();//禁用cors保护机制
http.cors().disable();//禁用session会话
http.sessionManagement().disable();//禁用form表单登录
http.formLogin().disable();//增加自定义验证过滤器(资源服务需要配置)
http.addFilter(newJwtVerificationFilter(super.authenticationManager(), prop));}}
7.全局异常处理
@RestControllerAdvicepublicclassGlobalExceptionHandler{@ExceptionHandler(AccessDeniedException.class)publicResultaccessDeniedException(){returnnewResult(403,"用户权限不足!",null);}@ExceptionHandler(RuntimeException.class)publicResultserverException(){returnnewResult(500,"服务出现异常!",null);}}
8.订单资源控制器
@RestController@RequestMapping("/order")publicclassOrderController{@Secured({"ROLE_ADMIN","ROLE_ORDER"})@RequestMapping("/info")publicStringinfo(){return"Order Controller ...";}}
9.启动类
@SpringBootApplication@EnableConfigurationProperties(RsaKeyProperties.class)publicclassSourceOrderApplication{publicstaticvoidmain(String[] args){SpringApplication.run(SourceOrderApplication.class, args);}}
七.产品资源
**直接复制订单服务,目录名称改为
sso-source-product
,然后修改yml配置文件、controller、启动类**
1.修改yml配置文件
- 改一下application-display-name、port 即可
server:port:9003servlet: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:truelogging:level:com.oyjp: debug
#自定义属性,配置公钥路径rsa:key:publicKeyPath: E:\auth_key\rsa_key.pub
2.产品资源控制器
- 编写产品的controller逻辑
@RestController@RequestMapping("/product")publicclassProductController{@Secured({"ROLE_ADMIN","ROLE_PRODUCT"})@RequestMapping("/info")publicStringinfo(){return"Productr Controller ...";}}
3.启动类
@SpringBootApplication@EnableConfigurationProperties(RsaKeyProperties.class)publicclassSourceProductApplication{publicstaticvoidmain(String[] args){SpringApplication.run(SourceProductApplication.class, args);}}
八 终极测试
1.认证服务测试
2.订单资源测试
3.产品资源测试
4.用户状态测试
张三
李四
王五
赵六
小七
老八
密码错误:
源码
链接:https://pan.baidu.com/s/1EINPwP4or0Nuj8BOEPsIyw
- 提取码:kbue
版权归原作者 墩墩分墩 所有, 如有侵权,请联系我们删除。