【SpringBoot】集成SpringSecurity+JWT实现多服务单点登录,原来这么easy
Spring Boot+Spring Security+JWT实现单点登录
源码
链接:https://pan.baidu.com/s/1EINPwP4or0Nuj8BOEPsIyw
- 提取码:kbue
一.概念
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.Signature
HMACSHA245(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.导入数据库
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);
四 通用模块
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());//JIDclaims.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.Response/Request工具类
/*** 请求工具类** @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";@Testpublic void generateKey() throws Exception {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-serverspring:datasource:driver-class-name: com.mysql.jdbc.Driverurl: jdbc:mysql://localhost:3306/security_test2?useSSL=falseusername: rootpassword: rootmybatis:type-aliases-package: com.oyjp.domainconfiguration: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)
public class RsaKeyProperties {private String publicKeyPath;private String privateKeyPath;private PublicKey publicKey;private PrivateKey privateKey;/*** 该方法用于初始化公钥和私钥的内容*/@PostConstructpublic 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@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return sysRoles;}/*** 是否账号已过期*/@JsonIgnore@Overridepublic boolean isAccountNonExpired() {return status != 1;}/*** 是否账号已被锁*/@JsonIgnore@Overridepublic boolean isAccountNonLocked() {return status != 2;}/*** 是否凭证已过期*/@JsonIgnore@Overridepublic boolean isCredentialsNonExpired() {return status != 3;}/*** 是否账号已禁用*/@JsonIgnore@Overridepublic boolean isEnabled() {return status != 4;}
}
角色类实现springSecurity的GrantedAuthority接口
@Data
public class SysRole implements GrantedAuthority {private Integer id;private String name;private String desc;@JsonIgnore@Overridepublic 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的UserDetailsService 接口,重新loadUserByUsername()
public interface SysUserDetailsService extends UserDetailsService {}
@Service
@Transactional
public class SysUserDetailsServiceImpl implements SysUserDetailsService {@Autowired(required = false)private SysUserMapper sysUserMapper;@Overridepublic 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;}@Overridepublic 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);}/*** 认证成功所执行的方法*/@Overrideprotected 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, "用户认证通过!");}/*** 认证失败所执行的方法*/@Overrideprotected 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 {@Autowiredprivate SysUserDetailsService sysUserDetailsService;@Autowiredprivate RsaKeyProperties prop;@Beanpublic BCryptPasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}public AuthenticationProvider daoAuthenticationProvider() {DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();//指定认证对象的来源daoAuthenticationProvider.setUserDetailsService(sysUserDetailsService);//指定密码编码的来源daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());daoAuthenticationProvider.setHideUserNotFoundExceptions(false);return daoAuthenticationProvider;}@Overridepublic void configure(AuthenticationManagerBuilder auth) throws Exception {auth.authenticationProvider(daoAuthenticationProvider());}@Overridepublic 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));}
}
六 订单资源
资源服务可以有很多个,这里只拿订单服务为例,记住,资源服务中只能通过公钥验证认证。不能签发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-orderspring:datasource:driver-class-name: com.mysql.jdbc.Driverurl: jdbc:mysql://localhost:3306/security_test2?useSSL=falseusername: rootpassword: rootmybatis:type-aliases-package: com.oyjp.domainconfiguration: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;/*** 该方法用于初始化公钥和私钥的内容*/@PostConstructpublic 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;}@Overridepublic 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的格式正确,则先要获取到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 = 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 {@Autowiredprivate RsaKeyProperties prop;@Overridepublic 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);}
}
七.产品资源
直接复制订单服务,目录名称改为sso-source-product,然后修改yml配置文件、controller、启动类
1.修改yml配置文件
- 改一下application-display-name、port 即可
server:port: 9003servlet:application-display-name: sso-source-productspring:datasource:driver-class-name: com.mysql.jdbc.Driverurl: jdbc:mysql://localhost:3306/security_test2?useSSL=falseusername: rootpassword: rootmybatis:type-aliases-package: com.oyjp.domainconfiguration:map-underscore-to-camel-case: true
logging:level:com.oyjp: debug#自定义属性,配置公钥路径
rsa:key:publicKeyPath: E:\auth_key\rsa_key.pub
2.产品资源控制器
- 编写产品的controller逻辑
@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);}
}
八 终极测试
1.认证服务测试

2.订单资源测试

3.产品资源测试

4.用户状态测试
张三

李四

王五

赵六

小七

老八

密码错误:

相关文章:
【SpringBoot】集成SpringSecurity+JWT实现多服务单点登录,原来这么easy
Spring BootSpring SecurityJWT实现单点登录 源码 链接:https://pan.baidu.com/s/1EINPwP4or0Nuj8BOEPsIyw 提取码:kbue 一.概念 1.1.SSO 介绍: 单点登录(SingleSignOn,SSO),当用户在身份认证服务器上登录一次以…...
手把手教你使用PLSQL远程连接Oracle数据库【内网穿透】
文章目录 前言1. 数据库搭建2. 内网穿透2.1 安装cpolar内网穿透2.2 创建隧道映射 3. 公网远程访问4. 配置固定TCP端口地址4.1 保留一个固定的公网TCP端口地址4.2 配置固定公网TCP端口地址4.3 测试使用固定TCP端口地址远程Oracle 前言 Oracle,是甲骨文公司的一款关系…...
浅谈Deep Learning 与 Machine Learning 与Artificial Intelligence
文章目录 三者的联系与区别 三者的联系与区别 “Deep Learning is a kind of Machine Learning, and Machine Learning is a kind of Artificial Intelligence.” 人工智能(AI),机器学习(Machine Learning,简称ML&am…...
和 Node.js 说拜拜,Deno零配置解决方案
不知道大家注意没有,在我们启动各种类型的 Node repo 时,root 目录很快就会被配置文件塞满。例如,在最新版本的 Next.js 中,我们就有 next.config.js、eslintrc.json、tsconfig.json 和 package.json。而在样式那边,还…...
AxureRP制作静态站点发布互联网,实现公网访问【内网穿透】
AxureRP制作静态站点发布互联网,内网穿透实现公网访问 文章目录 AxureRP制作静态站点发布互联网,内网穿透实现公网访问前言1.在AxureRP中生成HTML文件2.配置IIS服务3.添加防火墙安全策略4.使用cpolar内网穿透实现公网访问4.1 登录cpolar web ui管理界面4…...
【好文推荐】openGauss 5.0.0 数据库安全——全密态探究
前言 写此文章的目的,主要是验证: openGauss 5.0.0 数据库能够实现哪种加密方式的全密态全密态数据库的特点 一、全密态介绍 全密态数据库意在解决数据全生命周期的隐私保护问题,使得系统无论在何种业务场景和环境下,数据在传…...
堆的介绍与堆的实现和调整
个人主页:Lei宝啊 愿所有美好如期而遇 目录 堆的介绍: 关于堆的实现及相关的其他问题: 堆的初始化: 堆的销毁: 插入建堆: 堆向上调整: 交换两个节点的值: 堆向下调整&a…...
【广州华锐互动】马属直肠检查3D虚拟仿真课件
随着科技的发展,医疗行业也在不断地进行创新。其中,广州华锐互动开发的马属直肠检查3D虚拟仿真课件,为医学教育和实践操作带来了新的可能性。它不仅可以帮助医生提高诊断准确率,还可以让医学生在没有真实病人的情况下进行实践操作…...
Nuxt 菜鸟入门学习笔记:路由
文章目录 路由 Routing页面 Pages导航 Navigation路由参数 Route Parameters路由中间件 Route Middleware路由验证 Route Validation Nuxt 官网地址: https://nuxt.com/ 路由 Routing Nuxt 的一个核心功能是文件系统路由器。pages/目录下的每个 Vue 文件都会创建一…...
C++基本语法和注释
C程序介绍 C 程序可以定义为对象的集合,这些对象通过调用彼此的方法进行交互。现在让我们简要地看一下什么是类、对象,方法、即时变量。 对象 - 对象具有状态和行为。例如:一只狗的状态 - 颜色、名称、品种,行为 - 摇动、叫唤、吃…...
CSRF攻击
防御策略 过滤判断换referer头,添加tocken令牌验证,白名单 CSRF攻击和XSS比较 相同点:都是欺骗用户 不同点: XSS有攻击特征,所有输入点都要考虑代码,单引号过滤 CSRF没有攻击特征,利用的点…...
2023 “华为杯” 中国研究生数学建模竞赛(D题)深度剖析|数学建模完整代码+建模过程全解全析
问题一:区域碳排放量以及经济、人口、能源消费量的现状分析 思路: 定义碳排放量 Prediction 模型: CO2 P * (GDP/P) * (E/GDP) * (CO2/E) 其中: CO2:碳排放量 P:人口数量 GDP/P:人均GDP E/GDP:单位GDP能耗 CO2/E:单位能耗碳排放量 2.收集并统计相关…...
【Proteus仿真】【STM32单片机】基于单片机的智能晾衣架控制系统
文章目录 一、功能简介二、软件设计三、实验现象联系作者 一、功能简介 系统运行后,LCD1604显示传感器检测的温湿度、光线强度和风速,工作模式,以及相应阈值,系统工作状态等;系统默认为自动模式, 可通过K4…...
C/C++代码静态检测工具PC-Lint常见错误总结
目录 1、PC-Lint 概述 2、PC-lint 常见错误列举 3、PC-Lint报告的语法错误 4、总结 VC常用功能开发汇总(专栏文章列表,欢迎订阅,持续更新...)https://blog.csdn.net/chenlycly/article/details/124272585C软件异常排查从入门到…...
概率深度学习建模数据不确定性
https://zhuanlan.zhihu.com/p/568912284理解论文 What uncertainties do we need in Bayesian deep learning for computer vision? (NeurIPS 2017) [1]中的数据不确定性建模,并给出公式推导。论文[1]指出不确定性uncertainty分为随机不确定性(aleator…...
Jenkins自动化部署前后端分离项目 (svn + Springboot + Vue + maven)有图详解
1. 准备工作 本文的前后端分离项目,技术框架是: Springboot Vue Maven SVN Redis Mysql Nginx JDK 所以首先需要安装以下: 在腾讯云服务器OpenCLoudOS系统中安装jdk(有图详解) 在腾讯云服务器OpenCLoudOS系统…...
【ELK】日志系统部署
一、ELK日志分析系统 1、ELK的组成 ElasticSearchLogStashKibana ELK基于这三个开源日志的收集、存储、检索和可视化的解决方案;可帮助用户快速定位和分析应用程序的故障,监控应用程序性能和安全,以及提供丰富的数据分析和展示功能。 2、完…...
【算法挨揍日记】day08——30. 串联所有单词的子串、76. 最小覆盖子串
30. 串联所有单词的子串 30. 串联所有单词的子串 题目描述: 给定一个字符串 s 和一个字符串数组 words。 words 中所有字符串 长度相同。 s 中的 串联子串 是指一个包含 words 中所有字符串以任意顺序排列连接起来的子串。 例如,如果 words ["…...
SpringCloud Gateway--网关服务基本介绍和基本原理
😀前言 本篇博文是关于SpringCloud Gateway的基本介绍,希望你能够喜欢 🏠个人主页:晨犀主页 🧑个人简介:大家好,我是晨犀,希望我的文章可以帮助到大家,您的满意是我的动力…...
使用Vue-cli构建spa项目及结构解析
一,Vue-cli是什么? 是一个官方发布的Vue脚手架工具,用于快速搭建Vue项目结构,提供了现代前端开发所需要的一些基础功能,例如:Webpack打包、ESLint语法检查、单元测试、自动化部署等等。同时,Vu…...
label-studio的使用教程(导入本地路径)
文章目录 1. 准备环境2. 脚本启动2.1 Windows2.2 Linux 3. 安装label-studio机器学习后端3.1 pip安装(推荐)3.2 GitHub仓库安装 4. 后端配置4.1 yolo环境4.2 引入后端模型4.3 修改脚本4.4 启动后端 5. 标注工程5.1 创建工程5.2 配置图片路径5.3 配置工程类型标签5.4 配置模型5.…...
C++:std::is_convertible
C++标志库中提供is_convertible,可以测试一种类型是否可以转换为另一只类型: template <class From, class To> struct is_convertible; 使用举例: #include <iostream> #include <string>using namespace std;struct A { }; struct B : A { };int main…...
8k长序列建模,蛋白质语言模型Prot42仅利用目标蛋白序列即可生成高亲和力结合剂
蛋白质结合剂(如抗体、抑制肽)在疾病诊断、成像分析及靶向药物递送等关键场景中发挥着不可替代的作用。传统上,高特异性蛋白质结合剂的开发高度依赖噬菌体展示、定向进化等实验技术,但这类方法普遍面临资源消耗巨大、研发周期冗长…...
镜像里切换为普通用户
如果你登录远程虚拟机默认就是 root 用户,但你不希望用 root 权限运行 ns-3(这是对的,ns3 工具会拒绝 root),你可以按以下方法创建一个 非 root 用户账号 并切换到它运行 ns-3。 一次性解决方案:创建非 roo…...
实现弹窗随键盘上移居中
实现弹窗随键盘上移的核心思路 在Android中,可以通过监听键盘的显示和隐藏事件,动态调整弹窗的位置。关键点在于获取键盘高度,并计算剩余屏幕空间以重新定位弹窗。 // 在Activity或Fragment中设置键盘监听 val rootView findViewById<V…...
JVM虚拟机:内存结构、垃圾回收、性能优化
1、JVM虚拟机的简介 Java 虚拟机(Java Virtual Machine 简称:JVM)是运行所有 Java 程序的抽象计算机,是 Java 语言的运行环境,实现了 Java 程序的跨平台特性。JVM 屏蔽了与具体操作系统平台相关的信息,使得 Java 程序只需生成在 JVM 上运行的目标代码(字节码),就可以…...
A2A JS SDK 完整教程:快速入门指南
目录 什么是 A2A JS SDK?A2A JS 安装与设置A2A JS 核心概念创建你的第一个 A2A JS 代理A2A JS 服务端开发A2A JS 客户端使用A2A JS 高级特性A2A JS 最佳实践A2A JS 故障排除 什么是 A2A JS SDK? A2A JS SDK 是一个专为 JavaScript/TypeScript 开发者设计的强大库ÿ…...
Caliper 负载(Workload)详细解析
Caliper 负载(Workload)详细解析 负载(Workload)是 Caliper 性能测试的核心部分,它定义了测试期间要执行的具体合约调用行为和交易模式。下面我将全面深入地讲解负载的各个方面。 一、负载模块基本结构 一个典型的负载模块(如 workload.js)包含以下基本结构: use strict;/…...
libfmt: 现代C++的格式化工具库介绍与酷炫功能
libfmt: 现代C的格式化工具库介绍与酷炫功能 libfmt 是一个开源的C格式化库,提供了高效、安全的文本格式化功能,是C20中引入的std::format的基础实现。它比传统的printf和iostream更安全、更灵活、性能更好。 基本介绍 主要特点 类型安全:…...
零知开源——STM32F103RBT6驱动 ICM20948 九轴传感器及 vofa + 上位机可视化教程
STM32F1 本教程使用零知标准板(STM32F103RBT6)通过I2C驱动ICM20948九轴传感器,实现姿态解算,并通过串口将数据实时发送至VOFA上位机进行3D可视化。代码基于开源库修改优化,适合嵌入式及物联网开发者。在基础驱动上新增…...
