在前一篇文章当中,我们介绍了springBoot整合spring security单体应用版,在这篇文章当中,我将介绍springBoot整合spring secury+JWT实现单点登录与权限管理。

本文涉及的权限管理模型是基于资源的动态权限管理。数据库设计的表有 user 、role、user_role、permission、role_permission。

单点登录当中,关于访问者信息的存储有多种解决方案。如将其以key-value的形式存储于redis数据库中,访问者令牌中存放key。校验用户身份时,凭借访问者令牌中的key去redis中找value,没找到则返回“令牌已过期”,让访问者去(重新)认证。本文中的demo,是将访问者信息加密后存于token中返回给访问者,访问者携带令牌去访问服务时,服务提供者直接解密校验token即可。两种实现各有优缺点。大家也可以尝试着将本文中的demo的访问者信息存储改造成存在redis中的方式。文末提供完整的代码及sql脚本下载地址。

在进入正式步骤之前,我们需要了解以下知识点。

单点登录SSO

单点登录也称分布式认证,指的是在有多个系统的项目中,用户经过一次认证,即可访问该项目下彼此相互信任的系统。

单点登录流程

给大家画了个流程图

关于JWT

jwt,全称JSON Web Token,是一款出色的分布式身份校验方案。

jwt由三个部分组成

  1. 头部:主要设置一些规范信息,签名部分的编码格式就在头部中声明。
  2. 有效载荷:token中存放有效信息的部分,比如用户名,用户角色,过期时间等,但不适合放诸如密码等敏感数据,会造成泄露。
  3. 签名:将头部与载荷分别采用base64编码后,用“.”相连,再加入盐,最后使用头部声明的编码类型进行编码,就得到了签名。

jwt生成的Token安全性分析

想要使得token不被伪造,就要确保签名不被篡改。然而,其签名的头部和有效载荷使用base64编码,这与明文无异。因此,我们只能在盐上做手脚了。我们对盐进行非对称加密后,在将token发放给用户。

RSA非对称加密

  1. 基本原理:同时生成两把密钥:私钥和公钥,私钥隐秘保存,公钥可以下发给信任客户端 。

    • 公钥加密:只有私钥才能解密

    • 私钥加密:私钥或者公钥都能解密

  2. 优缺点:

    • 优点:安全、难以破解

    • 缺点:耗时,但是为了安全,这是可以接受的

SpringSecurity+JWT+RSA分布式认证思路分析

通过之前的学习,我们知道了spring security主要是基于过滤器链来做认证的,因此,如何打造我们的单点登录,突破口就在于spring security中的认证过滤器。

用户认证

在分布式项目当中,现在大多数都是前后端分离架构设计的,因此,我们需要能够接收POST请求的认证参数,而不是传统的表单提交。因此,我们需要修改修
改UsernamePasswordAuthenticationFilter过滤器中attemptAuthentication方法,让其能够接收请求体。

关于spring security的认证流程分析,大家可以参考我上一篇文章《Spring Security认证流程分析--练气后期》。

另外,默认情况下,successfulAuthentication 方法在通过认证后,直接将认证信息放到服务器的session当中就ok了。而我们分布式应用当中,前后端分离,禁用了session。因此,我们需要在认证通过后生成token(载荷内具有验证用户身份必要的信息)返回给用户。

身份校验

默认情况下,BasicAuthenticationFilter过滤器中doFilterInternal方法校验用户是否登录,就是看session中是否有用户信息。在分布式应用当中,我们要修改为,验证用户携带的token是否合法,并解析出用户信息,交给SpringSecurity,以便于后续的授权功能可以正常使用。

实现步骤

(默认大家一已经创建好了数据库)

第一步:创建一个springBoot的project

这个父工程主要做依赖的版本管理。

其pom.xml文件如下

<?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><modules><module>common</module></modules><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.3.3.RELEASE</version><relativePath/> <!-- lookup parent from repository --></parent><packaging>pom</packaging><groupId>pers.lbf</groupId><artifactId>springboot-springSecurity-jwt-rsa</artifactId><version>1.0.0-SNAPSHOT</version><properties><java.version>1.8</java.version><jwt.version>0.10.7</jwt.version><jackson.version>2.11.2</jackson.version><springboot.version>2.3.3.RELEASE</springboot.version><mybatis.version>2.1.3</mybatis.version><mysql.version>8.0.12</mysql.version><joda.version>2.10.5</joda.version><springSecurity.version>5.3.4.RELEASE</springSecurity.version><common.version>1.0.0-SNAPSHOT</common.version></properties><dependencyManagement><dependencies><dependency><groupId>pers.lbf</groupId><artifactId>common</artifactId><version>${common.version}</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId><version>${springboot.version}</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><version>${springboot.version}</version></dependency><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>${mybatis.version}</version></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>${mysql.version}</version></dependency><!--jwt所需jar包--><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-api</artifactId><version>${jwt.version}</version></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-impl</artifactId><version>${jwt.version}</version><scope>runtime</scope></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-jackson</artifactId><version>${jwt.version}</version><scope>runtime</scope></dependency>
<!--            处理日期--><dependency><groupId>joda-time</groupId><artifactId>joda-time</artifactId><version>${joda.version}</version></dependency><!--处理json工具包--><dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-databind</artifactId><version>${jackson.version}</version></dependency><!--日志包--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-logging</artifactId><version>${springboot.version}</version></dependency><!--测试包--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><version>${springboot.version}</version></dependency><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-test</artifactId><version>${springSecurity.version}</version></dependency></dependencies></dependencyManagement></project>

第二步:创建三个子模块

其中,common模块作为公共模块存在,提供基础服务,包括token的生成、rsa加密密钥的生成与使用、Json序列化与反序列化。

authentication-service模块提供单点登录服务(用户认证及授权)。

product-service模块模拟一个子系统。它主要负责提供接口调用和校验用户身份。

创建common模块模块

修改pom.xml,添加jwt、json等依赖

pom.xml

<?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-springSecurity-jwt-rsa</artifactId><groupId>pers.lbf</groupId><version>1.0.0-SNAPSHOT</version></parent><modelVersion>4.0.0</modelVersion><artifactId>common</artifactId><dependencies><!--jwt所需jar包--><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-api</artifactId></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-impl</artifactId><scope>runtime</scope></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-jackson</artifactId><scope>runtime</scope></dependency><dependency><groupId>joda-time</groupId><artifactId>joda-time</artifactId></dependency><!--处理json工具包--><dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-databind</artifactId></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></dependencies></project>

创建一个JSON工具类

**json工具类* @author 赖柄沣 bingfengdev@aliyun.com* @version 1.0* @date 2020/9/2 22:28*/
public class JsonUtils {public static final ObjectMapper MAPPER = new ObjectMapper();private static final Logger logger = LoggerFactory.getLogger(JsonUtils.class);private JsonUtils() {}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;}}
}

创建RSA加密工具类,并生成公钥和密钥文件

​ RsaUtils.java

/**RSA非对称加密工具类* @author 赖柄沣 bingfengdev@aliyun.com* @version 1.0* @date 2020/9/2 22:27*/
public class RsaUtils {private static final int DEFAULT_KEY_SIZE = 2048;/**从文件中读取公钥* @author 赖柄沣 bingfengdev@aliyun.com* @date 2020-09-04 13:10:15* @param filename 公钥保存路径,相对于classpath* @return java.security.PublicKey 公钥对象* @throws Exception* @version 1.0*/public static PublicKey getPublicKey(String filename) throws Exception {byte[] bytes = readFile(filename);return getPublicKey(bytes);}/**从文件中读取密钥* @author 赖柄沣 bingfengdev@aliyun.com* @date 2020-09-04 13:12:01* @param filename 私钥保存路径,相对于classpath* @return java.security.PrivateKey 私钥对象* @throws Exception* @version 1.0*/public static PrivateKey getPrivateKey(String filename) throws Exception {byte[] bytes = readFile(filename);return getPrivateKey(bytes);}/*** @author 赖柄沣 bingfengdev@aliyun.com* @date 2020-09-04 13:12:59* @param bytes 公钥的字节形式* @return java.security.PublicKey 公钥对象* @throws Exception* @version 1.0*/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);}/**获取密钥* @author 赖柄沣 bingfengdev@aliyun.com* @date 2020-09-04 13:14:02* @param bytes 私钥的字节形式* @return java.security.PrivateKey* @throws Exception* @version 1.0*/private static PrivateKey getPrivateKey(byte[] bytes) throws InvalidKeySpecException, NoSuchAlgorithmException {bytes = Base64.getDecoder().decode(bytes);PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(bytes);KeyFactory factory = KeyFactory.getInstance("RSA");return factory.generatePrivate(spec);}/*** 根据密文,生存rsa公钥和私钥,并写入指定文件*@author 赖柄沣 bingfengdev@aliyun.com*@date 2020-09-04 13:14:02* @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);}/**读文件* @author 赖柄沣 bingfengdev@aliyun.com* @date 2020-09-04 13:15:37* @param fileName* @return byte[]* @throws* @version 1.0*/private static byte[] readFile(String fileName) throws Exception {return Files.readAllBytes(new File(fileName).toPath());}/**写文件* @author 赖柄沣 bingfengdev@aliyun.com* @date 2020-09-04 13:16:01* @param destPath* @param bytes* @return void* @throws* @version 1.0*/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);}/**构造器私有化* @author 赖柄沣 bingfengdev@aliyun.com* @date 2020-09-04 13:16:29* @param* @return* @throws* @version 1.0*/private RsaUtils() {}}

生成私钥和公钥两个文件

/*** @author 赖柄沣 bingfengdev@aliyun.com* @version 1.0* @date 2020/9/3 10:28*/public class RsaTest {private String publicFile = "D:\\Desktop\\rsa_key.pub";private String privateFile = "D:\\Desktop\\rsa_key";/**生成公钥和私钥* @author 赖柄沣 bingfengdev@aliyun.com* @date 2020-09-03 10:32:16* @throws Exception* @version 1.0*/@Testpublic void generateKey() throws Exception{RsaUtils.generateKey(publicFile,privateFile,"Java开发实践",2048);}}

私钥文件

##### 创建token有效载荷实体类和JWT工具类

/**为了方便后期获取token中的用户信息,* 将token中载荷部分单独封装成一个对象* @author 赖柄沣 bingfengdev@aliyun.com* @version 1.0* @date 2020/9/2 22:24*/
public class Payload<T> implements Serializable {/*** token id*/private String id;/*** 用户信息(用户名、角色...)*/private T userInfo;/*** 令牌过期时间*/private Date expiration;
}

JwtUtils

/**token工具类* @author 赖柄沣 bingfengdev@aliyun.com* @version 1.0* @date 2020/9/2 22:28*/
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) throws ExpiredJwtException {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) throws ExpiredJwtException {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;}private JwtUtils() {}
}

写完common模块后,将其打包安装,后面的两个服务都需要引用。

创建认证服务模块authentication-service

认证服务模块的关键点在于自定义用户认证过滤器和用户校验过滤器,并将其加载到spring security的过滤器链中,替代掉默认的。

##### 修改pom.xml文件,添加相关依赖

pom.xml

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>pers.lbf</groupId><artifactId>springboot-springSecurity-jwt-rsa</artifactId><version>1.0.0-SNAPSHOT</version></parent><artifactId>authentication-service</artifactId><version>1.0.0-SNAPSHOT</version><name>authentication-service</name><description>Demo project for Spring Boot</description><properties><java.version>1.8</java.version></properties><dependencies><dependency><groupId>pers.lbf</groupId><artifactId>common</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope><exclusions><exclusion><groupId>org.junit.vintage</groupId><artifactId>junit-vintage-engine</artifactId></exclusion></exclusions></dependency><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-test</artifactId><scope>test</scope></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>

这个模块添加的依赖主要是springBoot整合spring security的相关依赖以及数据库相关的依赖,当然还有我们的common模块。

修改application.yml文件

这一步主要是设置数据库连接的信息以及公钥、私钥的位置信息

server:port: 8081
spring:datasource:url: jdbc:mysql://127.0.0.1:3306/security_authority?useSSL=false&serverTimezone=GMTusername: rootpassword: root1997driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:configuration:map-underscore-to-camel-case: true
logging:level:pers.lbf: debug
lbf:key:publicKeyPath: 你的公钥路径privateKeyPath: 你的私钥路径

配置解析公钥和私钥

**解析公钥和私钥的配置类* @author 赖柄沣 bingfengdev@aliyun.com* @version 1.0* @date 2020/9/3 10:42*/
@ConfigurationProperties(prefix = "lbf.key")
@ConstructorBinding
public class AuthServerRsaKeyProperties {private String publicKeyPath;private String privateKeyPath;private PublicKey publicKey;private PrivateKey privateKey;/**加载文件当中的公钥、私钥* 被@PostConstruct修饰的方法会在服务器加载Servlet的时候运行,* 并且只会被服务器执行一次。PostConstruct在构造函数之后执行,* init()方法之前执行。* @author 赖柄沣 bingfengdev@aliyun.com* @date 2020-09-03 12:07:35* @throws Exception e* @version 1.0*/@PostConstructpublic void loadKey() throws Exception {publicKey = RsaUtils.getPublicKey(publicKeyPath);privateKey = RsaUtils.getPrivateKey(privateKeyPath);}public String getPublicKeyPath() {return publicKeyPath;}public void setPublicKeyPath(String publicKeyPath) {this.publicKeyPath = publicKeyPath;}public String getPrivateKeyPath() {return privateKeyPath;}public void setPrivateKeyPath(String privateKeyPath) {this.privateKeyPath = privateKeyPath;}public PublicKey getPublicKey() {return publicKey;}public void setPublicKey(PublicKey publicKey) {this.publicKey = publicKey;}public PrivateKey getPrivateKey() {return privateKey;}public void setPrivateKey(PrivateKey privateKey) {this.privateKey = privateKey;}
}

修改启动类,添加token加密解析的配置和mapper扫描

/*** @author Ferryman*/
@SpringBootApplication
@MapperScan(value = "pers.lbf.ssjr.authenticationservice.dao")
@EnableConfigurationProperties(AuthServerRsaKeyProperties.class)
public class AuthenticationServiceApplication {public static void main(String[] args) {SpringApplication.run(AuthenticationServiceApplication.class, args);}}

创建用户登录对象UserLoginVO

我们将用户登录的请求参数封装到一个实体类当中,而不使用与数据库表对应的UserTO。

/**用户登录请求参数对象* @author 赖柄沣 bingfengdev@aliyun.com* @version 1.0* @date 2020/9/3 16:16*/
public class UserLoginVo implements Serializable {private String username;private String password;}

创建用户凭证对象UserAuthVO

这个对象主要用于存储访问者认证成功后,其在token中的信息。这里我们是不存储密码等敏感数据的。

/**用户凭证对象* @author 赖柄沣 bingfengdev@aliyun.com* @version 1.0* @date 2020/9/3 16:20*/
public class UserAuthVO implements Serializable {private String username;private List<SimpleGrantedAuthority> authorities;}

创建自定义认证过滤器

/**自定义认证过滤器* @author 赖柄沣 bingfengdev@aliyun.com* @version 1.0* @date 2020/9/3 12:11*/
public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {/*** 认证管理器*/private AuthenticationManager authenticationManager;private AuthServerRsaKeyProperties prop;/**构造注入* @author 赖柄沣 bingfengdev@aliyun.com* @date 2020-09-03 12:17:54* @param authenticationManager spring security的认证管理器* @param prop 公钥 私钥 配置类* @version 1.0*/public TokenLoginFilter(AuthenticationManager authenticationManager, AuthServerRsaKeyProperties prop) {this.authenticationManager = authenticationManager;this.prop = prop;}/**接收并解析用户凭证,并返回json数据* @author 赖柄沣 bingfengdev@aliyun.com* @date 2020-09-03 12:19:29* @param request req* @param response resp* @return Authentication* @version 1.0*/@Overridepublic Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response){//判断请求是否为POST,禁用GET请求提交数据if (!"POST".equals(request.getMethod())) {throw new AuthenticationServiceException("只支持POST请求方式");}//将json数据转换为java bean对象try {UserLoginVo user = new ObjectMapper().readValue(request.getInputStream(), UserLoginVo.class);if (user.getUsername()==null){user.setUsername("");}if (user.getPassword() == null) {user.setPassword("");}user.getUsername().trim();
//将用户信息交给spring security做认证操作return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(),user.getPassword()));}catch (Exception e) {throw new RuntimeException(e);}}/**这个方法会在验证成功时被调用*用户登录成功后,生成token,并且返回json数据给前端* @author 赖柄沣 bingfengdev@aliyun.com* @date 2020-09-03 13:00:23* @param request* @param response* @param chain* @param authResult* @version 1.0*/@Overrideprotected void successfulAuthentication(HttpServletRequest request,HttpServletResponse response, FilterChain chain, Authentication authResult) {//获取当前登录对象UserAuthVO user = new UserAuthVO();user.setUsername(authResult.getName());user.setAuthorities((List<SimpleGrantedAuthority>) authResult.getAuthorities());//使用jwt创建一个token,私钥加密String token = JwtUtils.generateTokenExpireInMinutes(user,prop.getPrivateKey(),15);//返回tokenresponse.addHeader("Authorization","Bearer"+token);//登录成功返回json数据提示try {//生成消息Map<String, Object> map = new HashMap<>();map.put("code",HttpServletResponse.SC_OK);map.put("msg","登录成功");//响应数据response.setContentType("application/json;charset=utf-8");response.setStatus(HttpServletResponse.SC_OK);PrintWriter writer = response.getWriter();writer.write(new ObjectMapper().writeValueAsString(map));writer.flush();writer.close();}catch (Exception e) {throw new RuntimeException(e);}}}

到了这一步,你或许会开始觉得难以理解,这需要你稍微了解spring security的认证流程

创建自定义校验过滤器

/**自定义身份验证器* @author 赖柄沣 bingfengdev@aliyun.com* @version 1.0* @date 2020/9/3 15:02*/
public class TokenVerifyFilter extends BasicAuthenticationFilter {private AuthServerRsaKeyProperties prop;public TokenVerifyFilter(AuthenticationManager authenticationManager, AuthServerRsaKeyProperties prop) {super(authenticationManager);this.prop = prop;}/**过滤请求* @author 赖柄沣 bingfengdev@aliyun.com* @date 2020-09-03 15:07:27* @param request* @param response* @param chain* @version 1.0*/@Overrideprotected void doFilterInternal(HttpServletRequest request,HttpServletResponse response, FilterChain chain) throws ServletException, IOException, AuthenticationException,ExpiredJwtException {//判断请求体的头中是否包含AuthorizationString authorization = request.getHeader("Authorization");//Authorization中是否包含Bearer,不包含直接返回if (authorization==null||!authorization.startsWith("Bearer")){chain.doFilter(request, response);return;}UsernamePasswordAuthenticationToken token;try {//解析jwt生成的token,获取权限token = getAuthentication(authorization);}catch (ExpiredJwtException e){// e.printStackTrace();chain.doFilter(request, response);return;}//获取后,将Authentication写入SecurityContextHolder中供后序使用SecurityContextHolder.getContext().setAuthentication(token);chain.doFilter(request, response);}/**对jwt生成的token进行解析* @author 赖柄沣 bingfengdev@aliyun.com* @date 2020-09-03 15:21:04* @param authorization auth* @return org.springframework.security.authentication.UsernamePasswordAuthenticationToken* @throws* @version 1.0*/public UsernamePasswordAuthenticationToken getAuthentication(String authorization) throws ExpiredJwtException{if (authorization == null) {return null;}Payload<UserAuthVO> payload;//从token中获取有效载荷payload = JwtUtils.getInfoFromToken(authorization.replace("Bearer", ""), prop.getPublicKey(), UserAuthVO.class);//获取当前访问对象UserAuthVO userInfo = payload.getUserInfo();if (userInfo == null){return null;}//将当前访问对象及其权限封装称spring security可识别的tokenUsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(userInfo,null,userInfo.getAuthorities());return token;}
}

编写spring security的配置类

这一步主要是是完成对spring security的配置。唯一和单体版应用集成spring'security不同的是,在这一步需要加入我们自定义的用户认证和用户校验的过滤器,还有就是禁用session。

/**spring security配置类* @author 赖柄沣 bingfengdev@aliyun.com* @version 1.0* @date 2020/9/3 15:41*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {@Autowiredprivate UserDetailsService userService;@Autowiredprivate AuthServerRsaKeyProperties properties;@Beanpublic BCryptPasswordEncoder myPasswordEncoder(){return new BCryptPasswordEncoder();}/**配置自定义过滤器* @author 赖柄沣 bingfengdev@aliyun.com* @date 2020-09-03 15:53:45* @param http* @version 1.0*/@Overrideprotected void configure(HttpSecurity http) throws Exception {//禁用跨域保护,取代它的是jwthttp.csrf().disable();//允许匿名访问的方法http.authorizeRequests().antMatchers("/login").anonymous();//其他需要鉴权//.anyRequest().authenticated();//添加认证过滤器http.addFilter(new TokenLoginFilter(authenticationManager(),properties));//添加验证过滤器http.addFilter(new TokenVerifyFilter(authenticationManager(),properties));//禁用session,前后端分离是无状态的http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);}/**配置密码加密策略* @author 赖柄沣 bingfengdev@aliyun.com* @date 2020-09-03 15:50:46* @param authenticationManagerBuilder* @version 1.0*/@Overrideprotected void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {authenticationManagerBuilder.userDetailsService(userService).passwordEncoder(myPasswordEncoder());}@Overridepublic void configure(WebSecurity webSecurity) throws Exception{//忽略静态资源webSecurity.ignoring().antMatchers("/assents/**","/login.html");}}

添加对GrantedAuthority类型的自定义反序列化工具

因为我们的权限信息是加密存储于token中的,因此要对authorities进行序列化与反序列化,然后由于jackson并不支持对其进行反序列化,因此需要我们自己去做。

*** @author 赖柄沣 bingfengdev@aliyun.com* @version 1.0* @date 2020/9/3 22:42*/
public class CustomAuthorityDeserializer extends JsonDeserializer {@Overridepublic Object deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {ObjectMapper mapper = (ObjectMapper) jp.getCodec();JsonNode jsonNode = mapper.readTree(jp);List<GrantedAuthority> grantedAuthorities = new LinkedList<>();Iterator<JsonNode> elements = jsonNode.elements();while (elements.hasNext()) {JsonNode next = elements.next();JsonNode authority = next.get("authority");grantedAuthorities.add(new SimpleGrantedAuthority(authority.asText()));}return grantedAuthorities;}}

在UserAuthVO上标记

/**用户凭证对象* @author 赖柄沣 bingfengdev@aliyun.com* @version 1.0* @date 2020/9/3 16:20*/
public class UserAuthVO implements Serializable {@JsonDeserialize(using = CustomAuthorityDeserializer.class)public void setAuthorities(List<SimpleGrantedAuthority> authorities) {this.authorities = authorities;}//省略了其他无关的代码
}

实现UserDetailsService接口

实现loadUserByUsername方法,修改认证信息获取方式为:从数据库中获取权限信息。

/*** @author 赖柄沣 bingfengdev@aliyun.com* @version 1.0* @date 2020/8/28 22:16*/
@Service("userService")
public class UserServiceImpl implements UserDetailsService {@Autowiredprivate IUserDao userDao;@Autowiredprivate IRoleDao roleDao;@Autowiredprivate IPermissonDao permissonDao;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {if (username == null){return null;}UserDO user = userDao.findByName(username);List<RoleDO> roleList = roleDao.findByUserId(user.getId());List<SimpleGrantedAuthority> list  = new ArrayList<> ();for (RoleDO roleDO : roleList) {List<PermissionDO> permissionListItems = permissonDao.findByRoleId(roleDO.getId());for (PermissionDO permissionDO : permissionListItems) {list.add(new SimpleGrantedAuthority(permissionDO.getPermissionUrl()));}}user.setAuthorityList(list);return user;}
}

提示:关于用户、角色、权限的数据库操作及其实体类到这里就省略了,不影响大家理解

自定义401和403异常处理

Spring Security 中的异常主要分为两大类:一类是认证异常,另一类是授权相关的异常。并且,其抛出异常的地方是在过滤器链中,如果你使用@ControllerAdvice是没有办法处理的。

当然,像spring security这么优秀的框架,当然考虑到了这个问题。

spring security当中的HttpSecurity提供的exceptionHandling() 方法用来提供异常处理。该方法构造出 ExceptionHandlingConfigurer异常处理配置类。

然后该类呢有提供了两个接口用于我们自定义异常处理:

  • AuthenticationEntryPoint 该类用来统一处理 AuthenticationException异常(403异常)
  • AccessDeniedHandler 该类用来统一处理 AccessDeniedException异常(401异常)

MyAuthenticationEntryPoint.java

/**401异常处理* @author 赖柄沣 bingfengdev@aliyun.com* @version 1.0* @date 2020/9/3 22:08*/
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {@Overridepublic void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {response.setContentType("application/json;charset=utf-8");response.setStatus(200);Map<String, Object> map = new HashMap<>();map.put("code", HttpServletResponse.SC_UNAUTHORIZED);map.put("msg","令牌已过期请重新登录");ServletOutputStream out = response.getOutputStream();String s = new ObjectMapper().writeValueAsString(map);byte[] bytes = s.getBytes();out.write(bytes);}
}

MyAccessDeniedHandler.java

/**403异常处理* @author 赖柄沣 bingfengdev@aliyun.com* @version 1.0* @date 2020/9/3 22:11*/
public class MyAccessDeniedHandler implements AccessDeniedHandler {@Overridepublic void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {response.setContentType("application/json;charset=utf-8");response.setStatus(200);Map<String, Object> map = new HashMap<>();map.put("code", HttpServletResponse.SC_FORBIDDEN);map.put("msg","未授权访问此资源,如有需要请联系管理员授权");ServletOutputStream out = response.getOutputStream();String s = new ObjectMapper().writeValueAsString(map);byte[] bytes = s.getBytes();out.write(bytes);}
}

将这两个类添加到spring security的配置当中

/**spring security配置类* @author 赖柄沣 bingfengdev@aliyun.com* @version 1.0* @date 2020/9/3 15:41*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {@Autowiredprivate UserDetailsService userService;@Autowiredprivate AuthServerRsaKeyProperties properties;@Beanpublic BCryptPasswordEncoder myPasswordEncoder(){return new BCryptPasswordEncoder();}/**配置自定义过滤器* @author 赖柄沣 bingfengdev@aliyun.com* @date 2020-09-03 15:53:45* @param http* @version 1.0*/@Overrideprotected void configure(HttpSecurity http) throws Exception {//其他代码。。。//添加自定义异常处理http.exceptionHandling().authenticationEntryPoint(new MyAuthenticationEntryPoint());http.exceptionHandling().accessDeniedHandler(new MyAccessDeniedHandler());//其他代码1}}

到这一步大家就可以运行启动类先进行测试一下。在本文当中就先将product-service模块也实现了再集中测试

创建子系统模块product-service

修改pom.xml文件

这一步和我们创建认证服务时相差无几。

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>pers.lbf</groupId><artifactId>springboot-springSecurity-jwt-rsa</artifactId><version>1.0.0-SNAPSHOT</version></parent><artifactId>product-service</artifactId><version>1.0.0-SNAPSHOT</version><name>product-service</name><description>Demo project for Spring Boot</description><properties><java.version>1.8</java.version></properties><dependencies><dependency><groupId>pers.lbf</groupId><artifactId>common</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope><exclusions><exclusion><groupId>org.junit.vintage</groupId><artifactId>junit-vintage-engine</artifactId></exclusion></exclusions></dependency><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-test</artifactId><scope>test</scope></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>

修改application.yml配置文件

这里主要是配置数据库信息和加入公钥的地址信息

server:port: 8082
spring:datasource:url: jdbc:mysql://127.0.0.1:3306/security_authority?useSSL=false&serverTimezone=GMTusername: rootpassword: root1997driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:configuration:map-underscore-to-camel-case: true
logging:level:pers.lbf: debug
lbf:key:publicKeyPath: 你的公钥地址

创建读取公钥的配置类

/**读取公钥配置类* @author 赖柄沣 bingfengdev@aliyun.com* @version 1.0* @date 2020/9/4 10:05*/
@ConfigurationProperties(prefix = "lbf.key")
@ConstructorBinding
public class ProductRsaKeyProperties {private String publicKeyPath;private PublicKey publicKey;@PostConstructpublic void loadKey() throws Exception {publicKey = RsaUtils.getPublicKey(publicKeyPath);}@Overridepublic String toString() {return "ProductRsaKeyProperties{" +"pubKeyPath='" + publicKeyPath + '\'' +", publicKey=" + publicKey +'}';}public String getPublicKeyPath() {return publicKeyPath;}public void setPublicKeyPath(String publicKeyPath) {this.publicKeyPath = publicKeyPath;}public PublicKey getPublicKey() {return publicKey;}public void setPublicKey(PublicKey publicKey) {this.publicKey = publicKey;}
}

修改启动类

这一步和创建认证服务器时一样,如要是加入公钥配置和mapper扫描

/*** @author Ferryman*/
@SpringBootApplication
@MapperScan(basePackages = "pers.lbf.ssjr.productservice.dao")
@EnableConfigurationProperties(ProductRsaKeyProperties.class)
public class ProductServiceApplication {public static void main(String[] args) {SpringApplication.run(ProductServiceApplication.class, args);}}

复制

这一步主要是将UserAuthVo、自定义校验器、自定义异常处理器和自定义反序列化器从认证服务模块复制过来。(之所以不放入到公共模块common中是因为。不想直接在common模块中引入springBoot整合spring security的依赖)

创建子模块spring security配置类

这里也只需要在认证服务模块的配置上修改即可,去掉自定义认证过滤器的内容。资源模块只负责校验,不做认证。

创建一个测试接口

/*** @author 赖柄沣 bingfengdev@aliyun.com* @version 1.0* @date 2020/8/27 20:02*/
@RestController
@RequestMapping("/product")
public class ProductController {@GetMapping("/get")@PreAuthorize("hasAuthority('product:get')")public String get() {return "产品信息接口调用成功!";}
}

第三步:启动项目,进行测试

登录(认证)操作

登录成功返回消息提示

并且可以在请求头中看到token

登陆失败提示"用户名或密码错误"

访问资源

携带令牌访问资源,且具备权限、令牌未过期

携带token访问资源。但是没有权限

未携带token访问(未登录、未经过认证)

携带过期令牌访问资源

写在最后

springBoot整合security实现权限管理与认证分布式版(前后端分离版)的的核心在于三个问题

  1. 禁用了session,用户信息保存在哪?

  2. 如何实现对访问者的认证,或者说是根据token去认证访问者?

  3. 如何实现对访问者的校验,或者说是根据token去校验访问者身份?

基本上我们解决了上面三个问题之后,springBoot整合spring security实现前后端分离(分布式)场景下的权限管理与认证问题我们就可以说是基本解决了。

springBoot整合spring security+JWT实现单点登录与权限管理前后端分离相关推荐

  1. springBoot整合spring security+JWT实现单点登录与权限管理前后端分离--筑基中期

    写在前面 在前一篇文章当中,我们介绍了springBoot整合spring security单体应用版,在这篇文章当中,我将介绍springBoot整合spring secury+JWT实现单点登录与 ...

  2. 基于shiro+jwt的真正rest url权限管理,前后端分离

    代码地址如下: http://www.demodashi.com/demo/13277.html bootshiro & usthe bootshiro是基于springboot+shiro+ ...

  3. SpringBoot整合Spring Security【超详细教程】

    好好学习,天天向上 本文已收录至我的Github仓库DayDayUP:github.com/Lee/DayDayUP,欢迎Star,更多文章请前往:目录导航 前言 Spring Security是一个 ...

  4. SpringBoot 整合 Spring Security 实现安全认证【SpringBoot系列9】

    SpringCloud 大型系列课程正在制作中,欢迎大家关注与提意见. 程序员每天的CV 与 板砖,也要知其所以然,本系列课程可以帮助初学者学习 SpringBooot 项目开发 与 SpringCl ...

  5. 八、springboot整合Spring Security

    springboot整合Spring Security 简介 Spring Security是一个功能强大且可高度自定义的身份验证和访问控制框架.它是保护基于Spring的应用程序的事实标准. Spr ...

  6. Spring boot 整合Spring Security Jwt

    记录学习Spring boot 整合Spring Security Jwt 学习参考 – 慢慢的干货 https://shimo.im/docs/OnZDwoxFFL8bnP1c/read 首先创建S ...

  7. Spring Security OAuth2 SSO 单点登录

    基于 Spring Security OAuth2 SSO 单点登录系统 SSO简介 单点登录(英语:Single sign-on,缩写为 SSO),又译为单一签入,一种对于许多相互关连,但是又是各自 ...

  8. 「单点登录与权限管理」系列概述

    首先,感谢几位朋友在朋友圈转发我的文章,小声的告诉你们,是我主动让他们帮忙转发的:)在朋友们的分享下,凌晨推送的一篇文章,阅读人数达到了280多,很满足,我会坚持写下去,分享工作.学习.生活中的所思所 ...

  9. 单点登录与权限管理本质:权限管理介绍

    前面几篇文章介绍了单点登录的本质,包括cookie.session.重定向的基本概念,单点登录的基本交互流程,cookie的重要性和安全问题.单点登录能够确保:必须通过身份验证后,才能访问网站,且访问 ...

最新文章

  1. 亲身经历,在实验室不要乱按按钮!!
  2. MQCache 秒开缓存快速入门指南 - 旁路(使用镜像交换机)
  3. Docker-compose 安装Jenkins
  4. Android----paint触摸轨迹监听
  5. asp.net登录状态验证
  6. java接收rowtype类型_Java PhysType.getJavaRowType方法代码示例
  7. 标准C程序设计七---120
  8. Android学习小Demo(14)Android中关于PopupWindow的使用
  9. HTML示例08---CSS3概述
  10. python教程视频完整版-Python教程视频完整版
  11. 技嘉GA7PESH3主板,官网驱动下载,官方使用说明
  12. Springboot日常游玩----logback的添加
  13. 我的Android进阶之旅------Android【设置】-【语言和输入法】-【语言】列表中找到相应语言所对应的列表项
  14. 开盘前1.5秒下单可追涨停股
  15. java 微信文章评论点赞_使用fiddler抓取微信公众号文章的阅读数、点赞数、评论数...
  16. 红色彼岸花计算机谱子,ceecceec
  17. js 事件绑定传入自定义参数
  18. 软考:嵌入式系统设计师——易错知识点总结
  19. js牛客网、赛码网输入输出
  20. Silvaco TCAD仿真8——网格mesh的意义(举例说明)

热门文章

  1. C#实现万年历(农历、节气、节日、星座、属相、生肖、闰年等)
  2. struts2+spring3+hibernate4
  3. 0.0.1 NODEMCU-ESP8266-12F VER 0.1CH340G开发版V3 4线oled0.96寸屏天气时钟日记
  4. 简报 | 俄罗斯为离岸地区制定特殊加密货币规则
  5. Angular 4 学习笔记1
  6. 告别纸币!人民币迎来大升级,央行试点区块链数字货币!
  7. 解决Google翻译不能使用的问题
  8. 裁判文书网 爬虫 最新更新2020-08-12
  9. 中国禁止“外国废物”可能有助于AI在美国的传播
  10. Ubuntu 18.04 版本中安装mysql 8的方法