4.SpringSecurity在分布式环境下的使用
参考
来源于黑马程序员: 手把手教你精通新版SpringSecurity
分布式认证概念说明
分布式认证,即我们常说的单点登录,简称SSO,指的是在多应用系统的项目中,用户只需要登录一次,就可以访 问所有互相信任的应用系统。
分布式认证流程图
首先,我们要明确,在分布式项目中,每台服务器都有各自独立的session,而这些session之间是无法直接共享资 源的,所以,session通常不能被作为单点登录的技术方案。最合理的单点登录方案流程如下图所示:
总结一下,单点登录的实现分两大环节:
- **用户认证:**这一环节主要是用户向认证服务器发起认证请求,认证服务器给用户返回一个成功的令牌token, 主要在认证服务器中完成,即图中的A系统,注意A系统只能有一个。
- **身份校验:**这一环节是用户携带token去访问其他服务器时,在其他服务器中要对token的真伪进行检验,主 要在资源服务器中完成,即图中的B系统,这里B系统可以有很多个。
JWT介绍
概念说明
从分布式认证流程中,我们不难发现,这中间起最关键作用的就是token,token的安全与否,直接关系到系统的 健壮性,这里我们选择使用JWT来实现token的生成和校验。 JWT,全称JSON Web Token,官网地址https://jwt.io,是一款出色的分布式身份校验方案。可以生成token,也可以解析检验token。
JWT生成的token由三部分组成
- 头部:主要设置一些规范信息,签名部分的编码格式就在头部中声明。
- 载荷:token中存放有效信息的部分,比如用户名,用户角色,过期时间等,但是不要放密码,会泄露!
- 签名:将头部与载荷分别采用base64编码后,用“.”相连,再加入盐,最后使用头部声明的编码类型进行编 码,就得到了签名。【通过随机盐在进行加密】
JWT生成token的安全性分析
从JWT生成的token组成上来看,要想避免token被伪造,主要就得看签名部分了,而签名部分又有三部分组成,其中头部和载荷的base64编码,几乎是透明的,毫无安全性可言,那么最终守护token安全的重担就落在了加入的盐上面了!
试想:如果生成token所用的盐与解析token时加入的盐是一样的。岂不是类似于中国人民银行把人民币防伪技术 公开了?大家可以用这个盐来解析token,就能用来伪造token。这时,我们就需要对盐采用非对称加密的方式进行加密,以达到生成token与校验token方所用的盐不一致的安全效果!
非对称加密RSA介绍
- **基本原理:**同时生成两把密钥:私钥和公钥,私钥隐秘保存,公钥可以下发给信任客户端
- 私钥加密,持有私钥或公钥才可以解密
- 公钥加密,持有私钥才可解密
- 优点:安全,难以破解
- 缺点:算法比较耗时,为了安全,可以接受
- 历史:三位数学家Rivest、Shamir 和 Adleman 设计了一种算法,可以实现非对称加密。这种算法用他们三 个人的名字缩写:RSA。
【总结】:也就是说,我们加密信息的时候,使用的是公钥,而验证token真伪的时候,使用的是公钥
JWT相关工具类
jar包
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-api</artifactId><version>0.10.7</version>
</dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-impl</artifactId><version>0.10.7</version><scope>runtime</scope>
</dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-jackson</artifactId><version>0.10.7</version><scope>runtime</scope>
</dependency>
载荷对象
/**
* 为了方便后期获取token中的用户信息,将token中载荷部分单独封装成一个对象
*/
@Data
public class Payload<T> {}
JWT工具类
/*** 生成token以及校验token相关方法*/
public class JwtUtils {private static final String JWT_PAYLOAD_USER_KEY = "user";/*** 私钥加密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)).setId(createJTI()).setExpiration(DateTime.now().plusMinutes(expire).toDate()).signWith(privateKey, SignatureAlgorithm.RS256).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.parser().setSigningKey(publicKey).parseClaimsJws(token);}private static String createJTI() {return new String(Base64.getEncoder().encode(UUID.randomUUID().toString().getBytes()));}/*** 获取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());claims.setUserInfo(JsonUtils.toBean(body.get(JWT_PAYLOAD_USER_KEY).toString(), userType));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;}
}
RSA工具类
非对称加密工具列
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);if (!dest.exists()) {dest.createNewFile();}Files.write(dest.toPath(), bytes);}
}
SpringSecurity+JWT+RSA分布式认证思路分析
SpringSecurity主要是通过过滤器来实现功能的!我们要找到SpringSecurity实现认证和校验身份的过滤器! 回顾集中式认证流程
用户认证
使用UsernamePasswordAuthenticationFilter过滤器中attemptAuthentication方法实现认证功能,该过滤 器父类中successfulAuthentication方法实现认证成功后的操作。
身份校验
使用BasicAuthenticationFilter过滤器中doFilterInternal方法验证是否登录,以决定能否进入后续过滤器。 分析分布式认证流程
用户认证
由于,分布式项目,多数是前后端分离的架构设计,我们要满足可以接受异步post的认证请求参数,需要修 改UsernamePasswordAuthenticationFilter过滤器中attemptAuthentication方法,让其能够接收请求体。
另外,默认successfulAuthentication方法在认证通过后,是把用户信息直接放入session就完事了,现在我 们需要修改这个方法,在认证通过后生成token并返回给用户。
身份校验
原来BasicAuthenticationFilter过滤器中doFilterInternal方法校验用户是否登录,就是看session中是否有用 户信息,我们要修改为,验证用户携带的token是否合法,并解析出用户信息,交给SpringSecurity,以便于 后续的授权功能可以正常使用。
SpringSecurity+JWT+RSA分布式认证实现
创建父工程并导入jar包
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.itheima</groupId><artifactId>springboot_security_jwt_rsa_parent</artifactId><packaging>pom</packaging><version>1.0-SNAPSHOT</version><modules><module>heima_common</module><module>heima_auth_server</module><module>heima_source_product</module></modules><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.1.3.RELEASE</version><relativePath/></parent>
</project>
通用模块
创建通用子模块并导入JWT相关jar包
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><parent><artifactId>springboot_security_jwt_rsa_parent</artifactId><groupId>com.itheima</groupId><version>1.0-SNAPSHOT</version></parent><modelVersion>4.0.0</modelVersion><artifactId>heima_common</artifactId><dependencies><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-api</artifactId><version>0.10.7</version></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-impl</artifactId><version>0.10.7</version><scope>runtime</scope></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-jackson</artifactId><version>0.10.7</version><scope>runtime</scope></dependency><!--jackson包--><dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-databind</artifactId><version>2.9.9</version></dependency><!--日志包--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-logging</artifactId></dependency><dependency><groupId>joda-time</groupId><artifactId>joda-time</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId></dependency></dependencies>
</project>
导入工具类
工具类如下
Payload.java
/*** 为了方便后期获取token中的用户信息,将token中载荷部分单独封装成一个对象*/
@Data
public class Payload<T> {private String id;private T userInfo;private Date expiration;
}
JsonUtil.java
public class JsonUtils {public static final ObjectMapper mapper = new ObjectMapper();private static final Logger logger = LoggerFactory.getLogger(JsonUtils.class);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;}}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;}}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;}}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;}}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;}}
}
jwtUitls.java
public class JwtUtils {private static final String JWT_PAYLOAD_USER_KEY = "user";/*** 私钥加密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)).setId(createJTI()).setExpiration(DateTime.now().plusMinutes(expire).toDate()).signWith(privateKey, SignatureAlgorithm.RS256).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.parser().setSigningKey(publicKey).parseClaimsJws(token);}private static String createJTI() {return new String(Base64.getEncoder().encode(UUID.randomUUID().toString().getBytes()));}/*** 获取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());claims.setUserInfo(JsonUtils.toBean(body.get(JWT_PAYLOAD_USER_KEY).toString(), userType));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;}
}
RsaUtils.java
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);if (!dest.exists()) {dest.createNewFile();}Files.write(dest.toPath(), bytes);}
}
在通用子模块中编写测试类生成rsa公钥和私钥
public class RsaUtilsTest {private String publicFile = "D:\\auth_key\\rsa_key.pub";private String privateFile = "D:\\auth_key\\rsa_key";@Testpublic void generateKey() throws Exception {RsaUtils.generateKey(publicFile, privateFile, "heima", 2048);}
}
执行后查看D:\auth_key目录发现私钥和公钥文件生成成功
认证服务
创建认证服务工程并导入jar包
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><parent><artifactId>springboot_security_jwt_rsa_parent</artifactId><groupId>com.itheima</groupId><version>1.0-SNAPSHOT</version></parent><modelVersion>4.0.0</modelVersion><artifactId>heima_auth_server</artifactId><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><dependency><groupId>com.itheima</groupId><artifactId>heima_common</artifactId><version>1.0-SNAPSHOT</version></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>5.1.47</version></dependency><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.1.0</version></dependency></dependencies>
</project>
创建认证服务配置文件
server:port: 9001
spring:datasource:driver-class-name: com.mysql.jdbc.Driverurl: jdbc:mysql:///security_authorityusername: rootpassword: root
mybatis:type-aliases-package: com.itheima.domainconfiguration:map-underscore-to-camel-case: true
logging:level:com.itheima: debug
rsa:key:pubKeyFile: D:\auth_key\id_key_rsa.pubpriKeyFile: D:\auth_key\id_key_rsa
提供解析公钥和私钥的配置类
@Data
@ConfigurationProperties(prefix = "heima.key")
public class RsaKeyProperties {private String pubKeyPath;private String priKeyPath;private PublicKey publicKey;private PrivateKey privateKey;@PostConstructpublic void loadKey() throws Exception {publicKey = RsaUtils.getPublicKey(pubKeyPath);privateKey = RsaUtils.getPrivateKey(priKeyPath);}
}
创建认证服务启动类
@SpringBootApplication
@MapperScan("com.itheima.mapper")
@EnableConfigurationProperties(RsaKeyProperties.class)
public class AuthApplication {public static void main(String[] args) {SpringApplication.run(AuthApplication.class, args);}
}
将上面集中式案例中数据库认证相关代码复制到认证服务中
需要复制的代码如果所示:
注意这里要去掉mapper中继承的通用mapper接口,处理器类上换成@RestController,这里前后端绝对分离,不能再跳转页面了,要返回数据。
public class JwtLoginFilter extends UsernamePasswordAuthenticationFilter {private AuthenticationManager authenticationManager;private RsaKeyProperties prop;public JwtLoginFilter(AuthenticationManager authenticationManager, RsaKeyProperties prop) {this.authenticationManager = authenticationManager;this.prop = prop;}public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {try {SysUser sysUser = new ObjectMapper().readValue(request.getInputStream(), SysUser.class);UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(sysUser.getUsername(), sysUser.getPassword());return authenticationManager.authenticate(authRequest);}catch (Exception e){try {response.setContentType("application/json;charset=utf-8");response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);PrintWriter out = response.getWriter();Map resultMap = new HashMap();resultMap.put("code", HttpServletResponse.SC_UNAUTHORIZED);resultMap.put("msg", "用户名或密码错误!");out.write(new ObjectMapper().writeValueAsString(resultMap));out.flush();out.close();}catch (Exception outEx){outEx.printStackTrace();}throw new RuntimeException(e);}}public void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {SysUser user = new SysUser();user.setUsername(authResult.getName());user.setRoles((List<SysRole>) authResult.getAuthorities());String token = JwtUtils.generateTokenExpireInMinutes(user, prop.getPrivateKey(), 24 * 60);response.addHeader("Authorization", "Bearer "+token);try {response.setContentType("application/json;charset=utf-8");response.setStatus(HttpServletResponse.SC_OK);PrintWriter out = response.getWriter();Map resultMap = new HashMap();resultMap.put("code", HttpServletResponse.SC_OK);resultMap.put("msg", "认证通过!");out.write(new ObjectMapper().writeValueAsString(resultMap));out.flush();out.close();}catch (Exception outEx){outEx.printStackTrace();}}
}
编写检验token过滤器
public class JwtVerifyFilter extends BasicAuthenticationFilter {private RsaKeyProperties prop;public JwtVerifyFilter(AuthenticationManager authenticationManager, RsaKeyProperties prop) {super(authenticationManager);this.prop = prop;}public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {String header = request.getHeader("Authorization");if (header == null || !header.startsWith("Bearer ")) {//如果携带错误的token,则给用户提示请登录!chain.doFilter(request, response);response.setContentType("application/json;charset=utf-8");response.setStatus(HttpServletResponse.SC_FORBIDDEN);PrintWriter out = response.getWriter();Map resultMap = new HashMap();resultMap.put("code", HttpServletResponse.SC_FORBIDDEN);resultMap.put("msg", "请登录!");out.write(new ObjectMapper().writeValueAsString(resultMap));out.flush();out.close();} else {//如果携带了正确格式的token要先得到tokenString token = header.replace("Bearer ", "");//验证tken是否正确Payload<SysUser> payload = JwtUtils.getInfoFromToken(token, prop.getPublicKey(), SysUser.class);SysUser user = payload.getUserInfo();if(user!=null){UsernamePasswordAuthenticationToken authResult = new UsernamePasswordAuthenticationToken(user.getUsername(), null, user.getAuthorities());SecurityContextHolder.getContext().setAuthentication(authResult);chain.doFilter(request, response);}}}
}
编写SpringSecurity配置类
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled=true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {@Autowiredprivate UserService userService;@Autowiredprivate RsaKeyProperties prop;@Beanpublic BCryptPasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder();}//指定认证对象的来源public void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userService).passwordEncoder(passwordEncoder());}//SpringSecurity配置信息public void configure(HttpSecurity http) throws Exception {http.csrf().disable().authorizeRequests().antMatchers("/product").hasAnyRole("USER").anyRequest().authenticated().and().addFilter(new JwtLoginFilter(super.authenticationManager(), prop)).addFilter(new JwtVerifyFilter(super.authenticationManager(), prop)).sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);}
}
启动测试认证服务
认证请求
认证通过结果
token在Headers中:
验证认证请求
资源服务
说明
资源服务可以有很多个,这里只拿产品服务为例,记住,资源服务中只能通过公钥验证认证。不能签发token!
创建产品服务并导入jar包
根据实际业务导包即可,咱们就暂时和认证服务一样了。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><parent><artifactId>springboot_security_jwt_rsa_parent</artifactId><groupId>com.itheima</groupId><version>1.0-SNAPSHOT</version></parent><modelVersion>4.0.0</modelVersion><artifactId>heima_source_product</artifactId><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><dependency><groupId>com.itheima</groupId><artifactId>heima_common</artifactId><version>1.0-SNAPSHOT</version></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>5.1.47</version></dependency><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.1.0</version></dependency></dependencies>
</project>
编写产品服务配置文件
切记这里只能有公钥地址!
server:port: 9002
spring:datasource:driver-class-name: com.mysql.jdbc.Driverurl: jdbc:mysql:///security_authorityusername: rootpassword: root
mybatis:type-aliases-package: com.itheima.domainconfiguration:map-underscore-to-camel-case: true
logging:level:com.itheima: debug
rsa:key:pubKeyFile: D:\auth_key\id_key_rsa.pub
编写读取公钥的配置类
@ConfigurationProperties("rsa.key")
public class RsaKeyProperties {private String pubKeyFile;private PublicKey publicKey;@PostConstructpublic void createRsaKey() throws Exception {publicKey = RsaUtils.getPublicKey(pubKeyFile);}public String getPubKeyFile() {return pubKeyFile;}public void setPubKeyFile(String pubKeyFile) {this.pubKeyFile = pubKeyFile;}public PublicKey getPublicKey() {return publicKey;}public void setPublicKey(PublicKey publicKey) {this.publicKey = publicKey;}
}
编写启动类
@SpringBootApplication
@MapperScan("com.itheima.mapper")
@EnableConfigurationProperties(RsaKeyProperties.class)
public class AuthSourceApplication {public static void main(String[] args) {SpringApplication.run(AuthSourceApplication.class, args);}
}
复制认证服务中,用户对象,角色对象和校验认证的接口
这时目录结构如图:
复制认证服务中SpringSecurity配置类做修改,去掉“增加自定义认证过滤器”即可!
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled=true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {@Autowiredprivate RsaKeyProperties prop;//SpringSecurity配置信息public void configure(HttpSecurity http) throws Exception {http.csrf().disable().authorizeRequests().antMatchers("/product").hasAnyRole("USER").anyRequest().authenticated().and().addFilter(new JwtVerifyFilter(super.authenticationManager(), prop)).sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);}
}
编写产品处理器
@RestController
@RequestMapping("/product")
public class ProductController {@GetMappingpublic String findAll(){return "产品测试成功!";}
}
启动产品服务做测试
携带token
在产品处理器上添加访问需要ADMIN角色
@RestController
@RequestMapping("/product")
public class ProductController {@Secured("ROLE_ADMIN")@GetMappingpublic String findAll(){return "产品测试成功!";}
}
重启测试权限不足
在数据库中手动给用户添加ADMIN角色
重新认证获取新token再测试OK了!
相关文章:

4.SpringSecurity在分布式环境下的使用
参考 来源于黑马程序员: 手把手教你精通新版SpringSecurity 分布式认证概念说明 分布式认证,即我们常说的单点登录,简称SSO,指的是在多应用系统的项目中,用户只需要登录一次,就可以访 问所有互相信任的应…...

RocketMQ和Kafka如何实现顺序写入和顺序消费?
0 前言 先说明kafka,顺序写入和消费是Kafka的重要特性,但需要正确的配置和使用方式才能保证。本文需要解释清楚Kafka如何通过分区来实现顺序性,以及生产者和消费者应该如何配合。 首先,顺序写入。Kafka的消息是按分区追加写入…...

SQL联合查询
文章目录 MySQL系列:1.内连接2.外连接3.自连接4.子查询5.合并查询6.插入查询 MySQL系列: 初识MySQL,MySQL常用数据类型和表的操作,增删改查(CRUD)操作(总),数据库约束数据库设计 #班级表 drop table if exists class; create ta…...
deepseek:三个月备考高级系统架构师
一、备考总体规划(2025年2月11日 - 2025年5月) 1. 第一阶段:基础夯实(2025年2月11日 - 2025年3月10日) 目标:快速掌握系统架构师考试的核心知识点。 重点内容: 计算机组成原理、操作系统、数据…...

支持向量机原理
支持向量机(简称SVM)虽然诞生只有短短的二十多年,但是自一诞生便由于它良好的分类性能席卷了机器学习领域。如果不考虑集成学习的算法,不考虑特定的训练数据集,尤其在分类任务中表现突出。在分类算法中的表现SVM说是排…...
DeepSeek人工智能AI汽车营销销售培训讲师培训师唐兴通讲课汽车销售大数据存量客户数字化营销数字化销售大模型销售话术引流内容社群私域
唐兴通 数字商业创新实践专家、数字营销与销售顾问 沃顿商学院特邀演讲嘉宾|美国营销协会艾菲奖评委 核心专长: AI商业化应用、数字营销创新、数字新销售能力体系打造、数字化转型、 教学经历:从教20年,执教12所全球顶尖商学院…...
Molecular Communication(分子通信)与 Molecular Semantic Communication(分子语义通信)
1. 引言 随着传统无线通信在极端环境(如微观生物体内、海洋深处)中的局限性凸显,分子通信(Molecular Communication, MC)成为一种新型通信范式。分子通信通过分子作为信息载体,在纳米尺度上传输信息&#…...
Webpack代码分割、分割策略性能优化详解
在前端面试中,Webpack 是一个常见的考察点,特别是关于性能优化、构建配置以及代码分割等方面的问题。以下是 Webpack 常见问题详解,包括 代码分割 相关的内容。 1. Webpack 基础概念 1.1 Webpack 是什么? Webpack 是一个前端构建工具,主要用于将项目中的各种资源(JavaS…...

大脑网络与智力:基于图神经网络的静息态fMRI数据分析方法|文献速递-医学影像人工智能进展
Title 题目 Brain networks and intelligence: A graph neural network based approach toresting state fMRI data 大脑网络与智力:基于图神经网络的静息态fMRI数据分析方法 01 文献速递介绍 智力是一个复杂的构念,包含了多种认知过程。研究人员通…...

ArcGIS Pro显示缓存空间不足导致编辑或加载数据显示不完全
ArcGIS Pro对于显示缓存有32GB的限制,所以当缓存设置中,缓存将达到32GB时,会出现编辑、加载slpk显示不全的情况。 清除计算机上的显示缓存方法 1.启动 ArcGlS Pro。单击左下角的设置,然后单击选项; 2.在选项窗口中&…...
天童美语:观察你的生活
在孩子的认知里,世界宛如一片充满神秘色彩的未知之境,有着无尽的奥秘等待他们去探索。家长们,引导孩子用心观察世界,领略其中的美妙,这对孩子的成长进程有着极为关键的作用。贵阳天童教育相信:观察生活&…...
网络通信的基石:深入理解 TCP/IP 协议栈与 TCP/UDP 协议
博文题目:网络通信的基石:深入理解 TCP/IP 协议栈与 TCP/UDP 协议 引言 在当今数字化世界中,网络已经渗透到我们生活的方方面面。从浏览网页、收发邮件,到在线视频、远程会议,所有这些便捷的网络应用都离不开一个至关重要的基础设施——TCP/IP 协议栈。它就像是互联网的…...

数据结构-栈和队列的应用
目录 前言一、栈的应用(迷宫问题)1.1 问题描述1.2 算法选择1.3 算法精化1.4 算法实现1.5 问题结果 二、队列的应用(农夫过河问题)2.1 问题描述2.2 算法选择2.3 算法精化2.4 算法实现2.5 问题结果 总结 前言 本篇文章使用两个例子…...

SpringBoot Bug 日志
Spring 循环依赖问题 Bug如下 wxMpConfiguration → subscribeHandler → wxMsgServiceImpl → wxMpConfiguration 解决方案 方案实施方式注意事项接口抽象定义 WxMpService 接口,通过接口注入最佳设计实践 Setter 注入对非必要依赖使用 setter 方法降低耦合度 L…...

halo发布文章的插件问题分析
前言 在准备发文到 halo 系统的时候提示错误如下,全是乱码 尝试将 halo 插件卸载后,再将插件目录下的文件全部删除 插件目录在 C:\Users\Administrator\.vscode\extensions\halo-dev.halo-1.3.0 然后再重新安装插件,在进行初始化的时候依然…...
2.5 模块化迁移策略:从传统项目到模块化系统
模块化迁移策略:从传统项目到模块化系统 将传统 Java 项目迁移至 JDK 9 模块化系统是一项系统性工程,需分阶段实施以降低风险。以下是详细的迁移策略、工具使用和实战示例。 1. 迁移阶段划分 阶段目标关键操作阶段1:兼容性验证确保项目能在…...

java商城解决方案
数字化时代,电子商务已成为企业拓展市场的重要渠道。对于想要建立在线商店的企业来说,选择正确的技术堆栈至关重要。 Java作为一种成熟且广泛使用的编程语言,为构建购物中心提供了强大的功能和灵活性。 商城Java源码:商城开发的核…...
算法-哈希表篇05-四数相加II
四数相加II 力扣题目链接 题目描述 给你四个整数数组 nums1、nums2、nums3 和 nums4 ,数组长度都是 n ,请你计算有多少个元组 (i, j, k, l) 能满足: 0 < i, j, k, l < n nums1[i] nums2[j] nums3[k] nums4[l] 0 解题思路 把数…...

WPS或word接入智能AI
DeepSeek接入WPS 配置WPS (1)下载 OfficeAl助手插件: 插件下载地址:https://www.office-ai.cn/。 安装插件后,打开WPS,菜单栏会新增"OfficeAl助手”选项卡。 如果没有出现, 左上找到文件菜单 -> 选项 ,在…...
Leetcode:学习记录
一、滑动窗口 1. 找出数组中元素和大于给定值的子数组的最小长度 右指针从左到右遍历,在每个右指针下,如果去掉左边元素的元素和大于等于给定值则左指针右移一次,直到小于给定值,右指针右移一个。 2.找到乘积小于给定值的子数组…...

XML Group端口详解
在XML数据映射过程中,经常需要对数据进行分组聚合操作。例如,当处理包含多个物料明细的XML文件时,可能需要将相同物料号的明细归为一组,或对相同物料号的数量进行求和计算。传统实现方式通常需要编写脚本代码,增加了开…...
[2025CVPR]DeepVideo-R1:基于难度感知回归GRPO的视频强化微调框架详解
突破视频大语言模型推理瓶颈,在多个视频基准上实现SOTA性能 一、核心问题与创新亮点 1.1 GRPO在视频任务中的两大挑战 安全措施依赖问题 GRPO使用min和clip函数限制策略更新幅度,导致: 梯度抑制:当新旧策略差异过大时梯度消失收敛困难:策略无法充分优化# 传统GRPO的梯…...
Java 语言特性(面试系列2)
一、SQL 基础 1. 复杂查询 (1)连接查询(JOIN) 内连接(INNER JOIN):返回两表匹配的记录。 SELECT e.name, d.dept_name FROM employees e INNER JOIN departments d ON e.dept_id d.dept_id; 左…...
Linux链表操作全解析
Linux C语言链表深度解析与实战技巧 一、链表基础概念与内核链表优势1.1 为什么使用链表?1.2 Linux 内核链表与用户态链表的区别 二、内核链表结构与宏解析常用宏/函数 三、内核链表的优点四、用户态链表示例五、双向循环链表在内核中的实现优势5.1 插入效率5.2 安全…...

JavaScript 中的 ES|QL:利用 Apache Arrow 工具
作者:来自 Elastic Jeffrey Rengifo 学习如何将 ES|QL 与 JavaScript 的 Apache Arrow 客户端工具一起使用。 想获得 Elastic 认证吗?了解下一期 Elasticsearch Engineer 培训的时间吧! Elasticsearch 拥有众多新功能,助你为自己…...
2024年赣州旅游投资集团社会招聘笔试真
2024年赣州旅游投资集团社会招聘笔试真 题 ( 满 分 1 0 0 分 时 间 1 2 0 分 钟 ) 一、单选题(每题只有一个正确答案,答错、不答或多答均不得分) 1.纪要的特点不包括()。 A.概括重点 B.指导传达 C. 客观纪实 D.有言必录 【答案】: D 2.1864年,()预言了电磁波的存在,并指出…...

PL0语法,分析器实现!
简介 PL/0 是一种简单的编程语言,通常用于教学编译原理。它的语法结构清晰,功能包括常量定义、变量声明、过程(子程序)定义以及基本的控制结构(如条件语句和循环语句)。 PL/0 语法规范 PL/0 是一种教学用的小型编程语言,由 Niklaus Wirth 设计,用于展示编译原理的核…...

初学 pytest 记录
安装 pip install pytest用例可以是函数也可以是类中的方法 def test_func():print()class TestAdd: # def __init__(self): 在 pytest 中不可以使用__init__方法 # self.cc 12345 pytest.mark.api def test_str(self):res add(1, 2)assert res 12def test_int(self):r…...

听写流程自动化实践,轻量级教育辅助
随着智能教育工具的发展,越来越多的传统学习方式正在被数字化、自动化所优化。听写作为语文、英语等学科中重要的基础训练形式,也迎来了更高效的解决方案。 这是一款轻量但功能强大的听写辅助工具。它是基于本地词库与可选在线语音引擎构建,…...
在 Spring Boot 项目里,MYSQL中json类型字段使用
前言: 因为程序特殊需求导致,需要mysql数据库存储json类型数据,因此记录一下使用流程 1.java实体中新增字段 private List<User> users 2.增加mybatis-plus注解 TableField(typeHandler FastjsonTypeHandler.class) private Lis…...