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,指的是在多应用系统的项目中,用户只需要登录一次,就可以访 问所有互相信任的应…...
使用 Notepad++ 编辑显示 MarkDown
Notepad 是一款免费的开源文本编辑器,专为 Windows 用户设计。它是替代记事本(Notepad)的最佳选择之一,因为它功能强大且轻量级。Notepad 支持多种编程语言和文件格式,并可以通过插件扩展其功能。 Notepad 是一款功能…...
Spring 框架数据库操作常见问题深度剖析与解决方案
Spring 框架数据库操作常见问题深度剖析与解决方案 在 Java 开发的广阔天地中,Spring 框架无疑是开发者们的得力助手,尤其在数据库操作方面,它提供了丰富且强大的功能。然而,就像任何技术一样,在实际项目开发过程中&a…...
第一天:爬虫介绍
每天上午9点左右更新一到两篇文章到专栏《Python爬虫训练营》中,对于爬虫有兴趣的伙伴可以订阅专栏一起学习,完全免费。 键盘为桨,代码作帆。这趟为期30天左右的Python爬虫特训即将启航,每日解锁新海域:从Requests库的…...
ECP在Successfactors中paylisp越南语乱码问题
导读 pyalisp:ECP中显示工资单有两种方式,一种是PE51,一种是hrform,PE51就是划线的那种, 海外使用的比较多,国内基本没人使用,hrform就是pdf,可以编辑pdf,这个国内相对使用的人 比…...
Express 中间件分类
一、 按功能用途分类 1. 应用级中间件 这类中间件应用于整个 Express 应用程序,会对每个进入应用的请求进行处理。通过 app.use() 方法挂载,可用于执行一些全局性的任务,像日志记录、请求预处理、设置响应头这类操作。 const express req…...
基于Multi-Runtime的云原生多态微服务:解耦基础设施与业务逻辑的革命性实践
引言:当微服务遭遇复杂性爆炸 在分布式系统复杂度指数级增长的今天,一线开发者平均需要处理27种不同的基础设施组件配置。CNCF最新研究报告指出,采用Multi-Runtime架构可减少83%的非功能性代码编写量,同时使分布式原语࿰…...
flutter isolate到底是啥
在 Flutter 中,Isolate 是一种实现多线程编程的机制,下面从概念、工作原理、使用场景、使用示例几个方面详细介绍: 概念 在 Dart 语言(Flutter 开发使用的编程语言)里,每个 Dart 程序至少运行在一个 Isol…...
Http connect timed out
客户向云端服务请求时,连接云端域名显示连接超时,为什么呢,偶尔会有。 java.net.SocketTimeoutException: connect timed out at java.net.PlainSocketImpl.socketConnect(Native Method) at java.net.AbstractPlainSocketI…...
Flutter 异步编程利器:Future 与 Stream 深度解析
目录 一、Future:处理单次异步操作 1. 概念解读 2. 使用场景 3. 基本用法 3.1 创建 Future 3.2 使用 then 消费 Future 3.3 特性 二、Stream:处理连续异步事件流 1. 概念解读 2. 使用场景 3. 基本用法 3.1 创建 Stream 3.2 监听 Stream 3.…...
langchain实现的内部问答系统及本地化替代方案
主旨:问答系统搭建使用langchain要token,本文目的在于一、解析langchain调用过程,二、不使用langchain规避token,而使用本地化部署的方案方向。主要是本地向量化库的建立。 文章目录 主旨:问答系统搭建使用langchain要…...
“新旗手”三星Galaxy S25系列,再次定义了AI手机的进化方向
一年多前,三星Galaxy S24系列正式发布,作为首款由Galaxy AI赋能的AI手机,带来了即圈即搜、通话实时翻译、AI扩图等“神奇能力”。 彼时AI手机还是一个新物种,没有明确的产品定义,体验上也没有“标准答案”。三星Galax…...
JVM的类加载器
什么是类加载器? 类加载器:JVM只会运行二进制文件,类加载器的作用就是将字节码文件加载到JVM中,从而Java 程序能够启动起来。 类加载器有哪些? 启动类加载器(BootStrap ClassLoader):加载JAVA HOME/jre/lib目录下的库…...
Node.js中的模块化:从原理到实践
目录 一、为什么需要模块化? 二、Node.js模块类型解析 2.1 核心模块 2.2 文件模块 2.3 第三方模块 三、CommonJS规范深度解析 3.1 模块加载原理 3.2 模块缓存机制 3.3 循环依赖处理 四、ES Modules新特性 4.1 基本用法 4.2 与CommonJS的差异比较 五、模…...
LLM论文笔记 6: Training Compute-Optimal Large Language Models
Arxiv日期:2022.3.29机构:Google DeepMind 关键词 scaling lawpower law参数量FLOPStokes 核心结论 1. 当前大多数大语言模型(如 GPT-3 和 Gopher)在计算预算分配上存在问题,模型参数过大而训练数据不足 2. 计算预算…...
数仓:核心概念,数仓系统(ETL,数仓分层,数仓建模),数仓建模方法(星型模型,雪花模型,星座模型)和步骤
数仓建模的核心概念 事实表(Fact Table): 存储业务过程的度量值(如销售额、订单数量等)。 通常包含外键,用于关联维度表。 维度表(Dimension Table): 存储描述性信息&…...
markdown|mermaid|typora绘制流程图的连接线类型怎么修改?
1、使用typora绘制流程图。别人例子里面的连线是圆弧,我的画出来就是带折线的 这是卖家秀: 这是买家秀: 无语了有没有? 犹豫了片刻我决定一探究竟(死磕)。 Typora --> 文件 --> 偏好设置 --》 mar…...
理解WebGPU 中的 GPUDevice :与 GPU 交互的核心接口
在 WebGPU 开发中, GPUDevice 是一个至关重要的对象,它是与 GPU 进行交互的核心接口。通过 GPUDevice ,开发者可以创建和管理 GPU 资源(如缓冲区、纹理、管线等),并提交命令缓冲区以执行渲染和计算任…...
庞氏骗局(Ponzi Scheme):金融投资与公司经营中的隐形陷阱(中英双语)
庞氏骗局:金融投资与公司经营中的隐形陷阱 庞氏骗局(Ponzi Scheme),这个词在金融史上屡见不鲜。从投资骗局到企业经营中的资本运作,庞氏骗局的核心逻辑始终如一:用后来的资金填补前面的缺口,营…...
【黑马点评】 使用RabbitMQ实现消息队列——3.批量获取1k个用户token,使用jmeter压力测试
【黑马点评】 使用RabbitMQ实现消息队列——3.批量获取用户token,使用jmeter压力测试 3.1 需求3.2 实现3.2.1 环境配置3.2.2 修改登录接口UserController和实现类3.2.3 测试类 3.3 使用jmeter进行测试3.4 测试结果3.5 将用户登录逻辑修改回去3.6 批量删除生成的用户…...
前端调用串口通信
项目录结构 node项目 1) 安装serialport npm install serialport 2)编写index.js 1 const SerialPort require(serialport); 2 var senddata [0x02];//串口索要发送的数据源 3 var port new SerialPort(COM3);//连接串口COM3 4 port.on(open, fun…...
微信小程序请求大模型监听数据块onChunkReceived方法把数据解析成json
自己写的真是案例,onChunkReceived监听到的数据块发现监听到的内容不只是一个块,有时候会是多块,所以自己加了一个循环解析的过程,不知道大家监听到的数据情况是否一致。 在网上翻阅大量资料有的说引入js文件,亲测无效…...
SQL Server STUFF 函数的用法及应用场景
在 SQL Server 中,STUFF 函数是一种强大的字符串处理工具,常用于删除指定位置的字符并插入新的字符。通过这个函数,开发者能够灵活地修改字符串,从而在数据处理、字符串拼接和格式化等方面大显身手。本文将深入探讨 STUFF 函数的语…...
Qt使用CipherSqlite插件访问加密的sqllite数据库
1.下载 git clone https://github.com/devbean/QtCipherSqlitePlugin.git 2.编译CipherSqlite插件 使用qt打开QtCipherSqlitePlugin项目,并构建插件 3.将构建的插件复制到安装目录 4.使用DB Browser (SQLCipher)创建数据库并加密 5.qt使用Ciphe…...
美国哈美顿零件号A203560 HAMILTON 10µl 1701 N CTC (22S/3) A200S 203560
零件号a61092-01 hamilton ml600 电源 110-220 vac 61092-01 零件号a81322 hamilton 1001 tll 1ml 注射器带塞子 81322 零件号a61710-01 hamilton ml600 探头支架 管架 61710-01 零件号a61614-01 hamilton ml600 填充管 12 ga 1219mm 4.57ml 61614-01 零件号a61615-01 ham…...
深入理解 Rust 的迭代器:从基础到高级
1. 迭代器的基础概念 1.1 什么是迭代器? 迭代器是一种设计模式,允许我们逐个访问集合中的元素,而无需暴露集合的内部结构。在 Rust 中,迭代器通过实现 Iterator trait 来定义。该 trait 主要包含一个方法: pub trai…...
聊聊 IP 地址和端口号的区别
在计算机网络中,两个基本概念对于理解设备如何通过网络进行通信至关重要。IP 地址和端口号是 TCP/IP 的典型特征,其定义如下:IP 地址是分配给连接到网络的每台机器的唯一地址,用于定位机器并与其通信。相反,端口号用于…...
地图打包注意事项
地图打包 (注意不能大写 后缀也不能) 需要文件 objects_n 文件夹 (内部大量png文件和plist文件).map.png 小地图tiles_n_n.plist 和tiles_n_n.png 文件sceneAtlasSplitConfigs合并.txt 文件放入 objects_n文件夹 内部(.png文件…...
代码随想录算法营Day38 | 62. 不同路径,63. 不同路径 II,343. 整数拆分,96. 不同的二叉搜索树
62. 不同路径 这题的限制是机器人在m x n的网格的左上角,每次只能向下走一格或者向右走一格。问到右下角有多少条不同路径。这个动态规划的初始状态是第一行和第一列的格子的值都是1,因为机器人只能向右走一格或者向下走一格,所以第一行和第…...
科普:数据仓库中的“指标”和“维度”
在数据仓库中,指标和维度是两个核心概念,它们对于数据分析和业务决策至关重要。以下是对这两个概念的分析及举例说明: 一、指标 定义: 指标是用于衡量业务绩效的关键数据点,通常用于监控、分析和优化企业的运营状况。…...
`Pinia` + `Formily` + `useTable` 实现搜索条件缓存方案
Pinia + Formily + useTable 实现搜索条件缓存方案 背景 在当前的应用体验中,每当用户刷新页面或退出系统时,之前的搜索条件就会消失不见。为了进一步提升工作效率并增强用户体验,希望能够实现这样一个功能:即使用户进行了页面刷新或是暂时离开了平台,再次返回时也能自动…...
Trader Joe‘s EDI 需求分析
Trader Joes成立于1967年,总部位于美国加利福尼亚州,是一家独特的零售商,专注于提供高质量且价格合理的食品。公司经营范围涵盖了各类杂货、冷冻食品、健康食品以及独特的本地特色商品。 EDI需求分析 电子数据交换(EDIÿ…...
【BUG】conda虚拟环境下,pip install安装直接到全局python目录中
问题描述 conda虚拟环境下,有的虚拟环境的python不能使用(which python时直接使用全局路径下的python),且pip install也会安装到全局路径中,无法安装到conda虚拟环境中。 解决方案 查看虚拟环境的PIP缓存默认路径&…...
用Shader glsl实现一个简单的PBR光照模型
PBR模型定义了各种光照属性,如基础颜色、金属度、粗糙度等,就像给物体设定各种 “性格特点”。顶点着色器负责把顶点从模型空间转换到裁剪空间,同时计算一些用于光照计算的参数,就像给顶点 “搬家” 并准备好 “行李”。而片段着色…...
Linux系统使用ollama本地安装部署DeepSeekR1 + open-webui
Linux系统使用ollama本地安装部署DeepSeekR1 open-webui 1. 首先,下载安装ollama #下载安装脚本并执行 curl -fsSL https://ollama.com/install.sh | sh #安装完成后查看ollama版本 ollama --version2. 使用ollama下载deepseek #不同的参数规格对硬件有不同的要…...
学习星开源在线考试教育系统
学习星开源考试系统 项目介绍 项目概述: 学习星在线考试系统是一款基于Java和Vue.js构建的前后端分离的在线考试解决方案。它旨在为教育机构、企业和个人提供一个高效、便捷的在线测试平台,支持多种题型,包括但不限于单选题、多选题、判断…...
树莓派通过手机热点,无线连接PC端电脑,进行远程操作
树莓派通过手机热点实现无线连接具有以下几点优势: 1.该方式能够联网,方便在项目开发时下载一些数据包。 2.该方式能够通过手机端查看树莓派IP地址(有些情况树莓派ip地址会发生改变) 借鉴链接如下: 树莓派的使用网线及无线连接方法及手机…...
【工业安全】-CVE-2022-35561- Tenda W6路由器 栈溢出漏洞
文章目录 1.漏洞描述 2.环境搭建 3.漏洞复现 4.漏洞分析 4.1:代码分析 4.2:流量分析 5.poc代码: 1.漏洞描述 漏洞编号:CVE-2022-35561 漏洞名称:Tenda W6 栈溢出漏洞 威胁等级:高危 漏洞详情࿱…...
Windows环境管理多个node版本
前言 在实际工作中,如果我们基于Windows系统开发,同时需要维护老项目,又要开发新项目,且不同项目依赖的node版本又不同时,那么就需要根据项目切换不同的版本。本文使用Node Version Manager(nvm࿰…...
git 沙盒 下(二)
url :Learn Git Branching 高级git 多次Rebase 最开始我先把bugFix分支先rebase到main上,之后再把c7合并到c6 ,之后就差合并为一个分支了,但是无论移动c7还是another分支都无法合并,都会在原地停留 后来根据提示最…...
基于JavaSpringmvc+myabtis+html的鲜花商城系统设计和实现
基于JavaSpringmvcmyabtishtml的鲜花商城系统设计和实现 🍅 作者主页 网顺技术团队 🍅 欢迎点赞 👍 收藏 ⭐留言 📝 🍅 文末获取源码联系方式 📝 🍅 查看下方微信号获取联系方式 承接各种定制系…...
vue-plugin-hiprint (vue2
页面效果 <template><div><div class"d-flex flex-column mt5"><div class"d-flex flex-row " style"margin-bottom: 10px;justify-content: center;"><!-- 纸张大小 A3、A4 等 --><div class"paper&quo…...
网站地址栏怎么变成HTTPS开头?
在当今的数字世界中,网络安全已成为不可或缺的一部分。对于网站管理员和所有者来说,确保访问者数据的安全和隐私至关重要。HTTPS是一种加密的通信协议,通过在客户端和服务器之间建立安全连接来保护数据传输。将网站从HTTP升级到HTTPS不仅提升…...
手机用流量怎样设置代理ip?
互联网各领域资料分享专区(不定期更新): Sheet...
Web渗透实战--XSS 常用语句以及绕过思路
Web渗透实战–XSS 常用语句以及绕过思路 0x01:干货 - XSS 测试常用标签语句 0x0101: 标签 <!-- 点击链接触发 - JavaScript 伪协议 --><a hrefjavascript:console.log(1)>XSS1</a> <!-- 字符编码绕过 - JavaScript 伪协议 -->&…...
哈希表-两个数的交集
代码随想录-刷题笔记 349. 两个数组的交集 - 力扣(LeetCode) 内容: 集合的使用 , 重复的数剔除掉,剩下的即为交集,最后加入数组即可。 class Solution {public int[] intersection(int[] nums1, int[] nums2) {Set<Integer…...
“数字+实体“双引擎发力树莓集团打造翠屏区首个智慧产业孵化标杆
2025 年 2 月 13 日,宜宾市翠屏区招商引资项目集中签约活动圆满举行。树莓集团产业合作总监童曦鸣代表企业出席并签约,将在翠屏区打造数字经济示范项目。 此次签约,树莓集团将以 “数字 实体” 双引擎发力。在数字产业化方面,凭…...
PostgreSQL 数据库压力测试指南
一、为什么需要压力测试? 数据库需要进行压力测试的原因主要包括以下几个方面: 性能评估:通过压力测试,可以了解数据库在高负载情况下的性能表现,包括响应时间、吞吐量和资源利用率等。这有助于确定系统的性能瓶颈。 …...
npm : 无法加载文件 C:\nvm\v20.11.1\npm.ps1,因为在此系统上禁止运行脚本。有关详细信息,请参阅
在vscode中运行前端项目时npm run dev 报错:npm : 无法加载文件 C:\nvm\v20.11.1\npm.ps1,因为在此系统上禁止运行脚本。有关详细信息,请参阅 https:/go.microsoft.com/fwlink/?LinkID135170 中的 about_Execution_Policies。 所在位置 行:1…...
-bash:/usr/bin/rm: Argument list too long 解决办法
问题概述 小文件日志太多导致无法使用rm命令,因为命令行参数列表的长度超过了系统允许的最大值。 需要删除/tmp目录下的所有文件,文件数量比较多。 ls -lt /tmp | wc -l 5682452 解决方法如下: 使用find -exec 遍历,然后执行删…...