Write in front
In the previous article, we introduced springBoot integrated spring security single application version. In this article, I will introduce springBoot integrated spring Security + JWT to realize single sign on and permission management.
The authority management model involved in this paper is resource-based dynamic authority management. The tables of database design include user, role and user_role,permission,role_permission.
There are many solutions for the storage of visitor information in single sign on. If it is stored in the redis database in the form of key value, the visitor token stores the key. When verifying the user's identity, use the key in the visitor's token to find value in redis. If it is not found, it will return "the token has expired" and ask the visitor to (RE) authenticate. The demo in this article is to encrypt the visitor's information and store it in the token and return it to the visitor. When the visitor carries the token to access the service, the service provider can directly decrypt and verify the token. The two implementations have their own advantages and disadvantages. You can also try to transform the visitor information storage of the demo in this article into a way of being stored in redis. At the end of the paper, the complete code and the download address of sql script are provided.
Before entering the formal steps, we need to understand the following knowledge points.
Single sign on SSO
Single sign on, also known as distributed authentication, refers to that in a project with multiple systems, users can access the systems trusted by each other under the project after one authentication.
Single sign on process
I drew a flow chart for you
About JWT
jwt, full name JSON Web Token, is an excellent distributed authentication scheme.
jwt consists of three parts
- Header: it mainly sets some specification information, and the coding format of the signature part is declared in the header.
- Payload: the part of the token that stores valid information, such as user name, user role, expiration time, etc., but it is not suitable for storing sensitive data such as password, which will cause disclosure.
- Signature: after the head and load are encoded with base64 respectively, use "." Connect, then add salt, and finally code with the coding type declared in the header to get the signature.
Security analysis of Token generated by jwt
If you want to prevent the token from being forged, you must ensure that the signature is not tampered with. However, the header and payload of its signature are encoded by base64, which is no different from plaintext. Therefore, we can only do things on salt. After asymmetric encryption of the salt, we issue the token to the user.
RSA asymmetric encryption
-
Basic principle: two keys are generated at the same time: private key and public key. The private key is kept secretly, and the public key can be distributed to the trusted client.
- Public key encryption: only the private key can be decrypted
- Private key encryption: both private key and public key can be decrypted
-
Advantages and disadvantages:
- Advantages: safe and difficult to crack
- Disadvantages: time consuming, but for safety, this is acceptable
Analysis of spring Security + JWT + RSA distributed authentication
Through the previous study, we know that spring security is mainly based on the filter chain for authentication. Therefore, how to build our single sign on, the breakthrough lies in the authentication filter in spring security.
User authentication
In distributed projects, most of them are designed with front-end and back-end separation architecture. Therefore, we need authentication parameters that can receive POST requests, rather than traditional form submission. Therefore, we need to modify it
Change the attemptAuthentication method in the UsernamePasswordAuthenticationFilter filter so that it can receive the request body.
For the authentication process analysis of spring security, you can refer to my last article< Analysis of Spring Security certification process -- later stage of gas training>.
In addition, by default, the successful authentication method will directly put the authentication information into the server session after passing the authentication. In our distributed applications, the front and back ends are separated and session is disabled. Therefore, we need to generate a t ok en (the payload has the necessary information to verify the user's identity) and return it to the user after passing the authentication.
Identity Check
By default, the doFilterInternal method in the BasicAuthenticationFilter checks whether the user logs in, that is, whether there is user information in the session. In distributed applications, we need to modify it to verify whether the token carried by the user is legal, analyze the user information and give it to spring security, so that the subsequent authorization functions can be used normally.
Implementation steps
(the database has been created by default)
Step 1: create a springBoot project
This parent project is mainly used for dependent version management.
Its POM The XML file is as follows
<?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 what is needed jar package--> <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> <!-- Processing date--> <dependency> <groupId>joda-time</groupId> <artifactId>joda-time</artifactId> <version>${joda.version}</version> </dependency> <!--handle json tool kit--> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>${jackson.version}</version> </dependency> <!--Log package--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-logging</artifactId> <version>${springboot.version}</version> </dependency> <!--Test package--> <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>
Step 2: create three sub modules
Among them, the common module exists as a public module and provides basic services, including token generation, rsa encryption key generation and use, Json serialization and deserialization.
The authentication service module provides single sign on services (user authentication and authorization).
The product service module simulates a subsystem. It is mainly responsible for providing interface calls and verifying user identity.
Create common module
Modify POM XML, add jwt, json and other dependencies
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 what is needed jar package--> <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> <!--handle json tool kit--> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </dependency> <!--Log package--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-logging</artifactId> </dependency> <!--Test package--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency> </dependencies> </project>
Create a JSON tool class
**json Tool class * @author Lai Bingfeng 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 Serialization error:" + 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 Parsing error:" + 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 Parsing error:" + 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 Parsing error:" + 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 Parsing error:" + json, e); return null; } } }
Create RSA encryption tool class and generate public key and key files
RsaUtils.java
/**RSA Asymmetric encryption tool class * @author Lai Bingfeng bingfengdev@aliyun.com * @version 1.0 * @date 2020/9/2 22:27 */ public class RsaUtils { private static final int DEFAULT_KEY_SIZE = 2048; /**Read public key from file * @author Lai Bingfeng bingfengdev@aliyun.com * @date 2020-09-04 13:10:15 * @param filename Public key saving path, relative to classpath * @return java.security.PublicKey Public key object * @throws Exception * @version 1.0 */ public static PublicKey getPublicKey(String filename) throws Exception { byte[] bytes = readFile(filename); return getPublicKey(bytes); } /**Read key from file * @author Lai Bingfeng bingfengdev@aliyun.com * @date 2020-09-04 13:12:01 * @param filename Private key saving path, relative to classpath * @return java.security.PrivateKey Private key object * @throws Exception * @version 1.0 */ public static PrivateKey getPrivateKey(String filename) throws Exception { byte[] bytes = readFile(filename); return getPrivateKey(bytes); } /** * @author Lai Bingfeng bingfengdev@aliyun.com * @date 2020-09-04 13:12:59 * @param bytes Byte form of public key * @return java.security.PublicKey Public key object * @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); } /**Get key * @author Lai Bingfeng bingfengdev@aliyun.com * @date 2020-09-04 13:14:02 * @param bytes Byte form of private key * @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); } /** * According to the ciphertext, the rsa public key and private key are saved and written to the specified file *@author Lai Bingfeng bingfengdev@aliyun.com *@date 2020-09-04 13:14:02 * @param publicKeyFilename Public key file path * @param privateKeyFilename Private key file path * @param secret Generate ciphertext of key */ 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(); // Get the public key and write it out byte[] publicKeyBytes = keyPair.getPublic().getEncoded(); publicKeyBytes = Base64.getEncoder().encode(publicKeyBytes); writeFile(publicKeyFilename, publicKeyBytes); // Get the private key and write it out byte[] privateKeyBytes = keyPair.getPrivate().getEncoded(); privateKeyBytes = Base64.getEncoder().encode(privateKeyBytes); writeFile(privateKeyFilename, privateKeyBytes); } /**read file * @author Lai Bingfeng 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()); } /**Write file * @author Lai Bingfeng 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); } /**Constructor privatization * @author Lai Bingfeng bingfengdev@aliyun.com * @date 2020-09-04 13:16:29 * @param * @return * @throws * @version 1.0 */ private RsaUtils() { } }
Generate two files: private key and public key
/** * @author Lai Bingfeng 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"; /**Generate public and private keys * @author Lai Bingfeng bingfengdev@aliyun.com * @date 2020-09-03 10:32:16 * @throws Exception * @version 1.0 */ @Test public void generateKey() throws Exception{ RsaUtils.generateKey(publicFile,privateFile,"Java Development practice",2048); } }
The private key file must be protected!!!
The private key file must be protected!!!
The private key file must be protected!!!
(say important things three times!!!)
##### Create token payload entity class and JWT tool class
/**In order to facilitate later access to the user information in the token, * Encapsulate the load part of the token into an object * @author Lai Bingfeng bingfengdev@aliyun.com * @version 1.0 * @date 2020/9/2 22:24 */ public class Payload<T> implements Serializable { /** * token id */ private String id; /** * User information (user name, role...) */ private T userInfo; /** * Token expiration time */ private Date expiration; getter. . . setter. . . }
JwtUtils
/**token Tool class * @author Lai Bingfeng bingfengdev@aliyun.com * @version 1.0 * @date 2020/9/2 22:28 */ public class JwtUtils { private static final String JWT_PAYLOAD_USER_KEY = "user"; /** * Private key encryption token * * @param userInfo Data in load * @param privateKey Private key * @param expire Expiration time, in minutes * @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(); } /** * Private key encryption token * * @param userInfo Data in load * @param privateKey Private key * @param expire Expiration time in seconds * @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(); } /** * Public key parsing token * * @param token token in user request * @param publicKey Public key * @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())); } /** * Get the user information in the token * * @param token Token in user request * @param publicKey Public key * @return User information */ 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; } /** * Get the load information in the token * * @param token Token in user request * @param publicKey Public key * @return User information */ 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() { } }
After writing the common module, package it and install it. The following two services need to be referenced.
Create authentication service module authentication service
The key point of the authentication service module is to customize the user authentication filter and user verification filter, and load them into the filter chain of spring security to replace the default.
##### Modify POM XML file, add related dependencies
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>
The added dependencies of this module are mainly related to springBoot integration, spring security and database. Of course, there is our common module.
Modify application YML file
This step is mainly to set the information of database connection and the location information of public key and private key
server: port: 8081 spring: datasource: url: jdbc:mysql://127.0.0.1:3306/security_authority?useSSL=false&serverTimezone=GMT username: root password: root1997 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: configuration: map-underscore-to-camel-case: true logging: level: pers.lbf: debug lbf: key: publicKeyPath: Your public key path privateKeyPath: Your private key path
Configure public and private key resolution
**Configuration class for resolving public and private keys * @author Lai Bingfeng 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; /**Load the public key and private key in the file * The method modified by @ PostConstruct will run when the server loads the Servlet, * And will only be executed once by the server. PostConstruct is executed after the constructor, * init()Method. * @author Lai Bingfeng bingfengdev@aliyun.com * @date 2020-09-03 12:07:35 * @throws Exception e * @version 1.0 */ @PostConstruct public 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; } }
Modify the startup class and add the configuration of token encryption resolution and mapper scanning
/** * @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); } }
Create user login object UserLoginVO
We encapsulate the request parameters of user login into an entity class without using the UserTO corresponding to the database table.
/**User login request parameter object * @author Lai Bingfeng bingfengdev@aliyun.com * @version 1.0 * @date 2020/9/3 16:16 */ public class UserLoginVo implements Serializable { private String username; private String password; getter. . . settter. . . }
Create user credential object UserAuthVO
This object is mainly used to store the information of the visitor in the token after successful authentication. Here we do not store passwords and other sensitive data.
/**User credential object * @author Lai Bingfeng bingfengdev@aliyun.com * @version 1.0 * @date 2020/9/3 16:20 */ public class UserAuthVO implements Serializable { private String username; private List<SimpleGrantedAuthority> authorities; getter. . . setter. . . }
Create a custom authentication filter
/**Custom authentication filter * @author Lai Bingfeng bingfengdev@aliyun.com * @version 1.0 * @date 2020/9/3 12:11 */ public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter { /** * Authentication manager */ private AuthenticationManager authenticationManager; private AuthServerRsaKeyProperties prop; /**Structural injection * @author Lai Bingfeng bingfengdev@aliyun.com * @date 2020-09-03 12:17:54 * @param authenticationManager spring security Authentication manager for * @param prop Public and private key configuration class * @version 1.0 */ public TokenLoginFilter(AuthenticationManager authenticationManager, AuthServerRsaKeyProperties prop) { this.authenticationManager = authenticationManager; this.prop = prop; } /**Receive and parse user credentials and return json data * @author Lai Bingfeng bingfengdev@aliyun.com * @date 2020-09-03 12:19:29 * @param request req * @param response resp * @return Authentication * @version 1.0 */ @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response){ //Judge whether the request is POST, and disable the data submission of GET request if (!"POST".equals(request.getMethod())) { throw new AuthenticationServiceException( "Only support POST Request mode"); } //Convert json data into java bean objects 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(); //Give the user information to spring security for authentication return authenticationManager.authenticate( new UsernamePasswordAuthenticationToken( user.getUsername(), user.getPassword())); }catch (Exception e) { throw new RuntimeException(e); } } /**This method will be called when the verification is successful *After the user logs in successfully, a token is generated and json data is returned to the front end * @author Lai Bingfeng bingfengdev@aliyun.com * @date 2020-09-03 13:00:23 * @param request * @param response * @param chain * @param authResult * @version 1.0 */ @Override protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) { //Get the current login object UserAuthVO user = new UserAuthVO(); user.setUsername(authResult.getName()); user.setAuthorities((List<SimpleGrantedAuthority>) authResult.getAuthorities()); //Use jwt to create a token and encrypt the private key String token = JwtUtils.generateTokenExpireInMinutes(user,prop.getPrivateKey(),15); //Return token response.addHeader("Authorization","Bearer"+token); //json data prompt returned after successful login try { //Generate message Map<String, Object> map = new HashMap<>(); map.put("code",HttpServletResponse.SC_OK); map.put("msg","Login successful"); //Response data 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); } } }
At this stage, you may begin to feel difficult to understand, which requires you to have a little understanding of the authentication process of spring security. You can read my previous articles Analysis of Spring Security certification process -- later stage of gas training.
Create a custom check filter
/**Custom authenticator * @author Lai Bingfeng 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; } /**Filter request * @author Lai Bingfeng bingfengdev@aliyun.com * @date 2020-09-03 15:07:27 * @param request * @param response * @param chain * @version 1.0 */ @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException, AuthenticationException,ExpiredJwtException { //Judge whether Authorization is included in the header of the request body String authorization = request.getHeader("Authorization"); //Whether the Authorization contains Bearer and does not include direct return if (authorization==null||!authorization.startsWith("Bearer")){ chain.doFilter(request, response); return; } UsernamePasswordAuthenticationToken token; try { //Parse the token generated by jwt and obtain permission token = getAuthentication(authorization); }catch (ExpiredJwtException e){ // e.printStackTrace(); chain.doFilter(request, response); return; } //After obtaining, write the Authentication into the SecurityContextHolder for subsequent use SecurityContextHolder.getContext().setAuthentication(token); chain.doFilter(request, response); } /**Parse the token generated by jwt * @author Lai Bingfeng 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; //Get payload from token payload = JwtUtils.getInfoFromToken(authorization.replace("Bearer", ""), prop.getPublicKey(), UserAuthVO.class); //Get current access object UserAuthVO userInfo = payload.getUserInfo(); if (userInfo == null){ return null; } //Encapsulate the current access object and its permission as a token recognized by spring security UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(userInfo,null,userInfo.getAuthorities()); return token; } }
Write the configuration class of spring security
This step is mainly to complete the configuration of spring security. The only difference from the application integration spring'security of the single version is that in this step, we need to add our customized user authentication and user verification filters, and disable session.
/**spring security Configuration class * @author Lai Bingfeng bingfengdev@aliyun.com * @version 1.0 * @date 2020/9/3 15:41 */ @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class SpringSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsService userService; @Autowired private AuthServerRsaKeyProperties properties; @Bean public BCryptPasswordEncoder myPasswordEncoder(){ return new BCryptPasswordEncoder(); } /**Configure custom filters * @author Lai Bingfeng bingfengdev@aliyun.com * @date 2020-09-03 15:53:45 * @param http * @version 1.0 */ @Override protected void configure(HttpSecurity http) throws Exception { //Cross domain protection is disabled and replaced by jwt http.csrf().disable(); //Methods that allow anonymous access http.authorizeRequests().antMatchers("/login").anonymous(); //Other authentication required //.anyRequest().authenticated(); //Add authentication filter http.addFilter(new TokenLoginFilter(authenticationManager(),properties)); //Add validation filter http.addFilter(new TokenVerifyFilter(authenticationManager(),properties)); //If session is disabled, the separation of front and rear ends is stateless http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); } /**Configure password encryption policy * @author Lai Bingfeng bingfengdev@aliyun.com * @date 2020-09-03 15:50:46 * @param authenticationManagerBuilder * @version 1.0 */ @Override protected void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception { authenticationManagerBuilder.userDetailsService(userService).passwordEncoder(myPasswordEncoder()); } @Override public void configure(WebSecurity webSecurity) throws Exception{ //Ignore static resources webSecurity.ignoring().antMatchers("/assents/**","/login.html"); } }
Add a custom deserialization tool for the GrantedAuthority type
Because our permission information is encrypted and stored in the token, we need to serialize and deserialize the authorities. Since jackson does not support deserialization, we need to do it ourselves.
** * @author Lai Bingfeng bingfengdev@aliyun.com * @version 1.0 * @date 2020/9/3 22:42 */ public class CustomAuthorityDeserializer extends JsonDeserializer { @Override public 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; } }
Mark on UserAuthVO
/**User credential object * @author Lai Bingfeng 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; } //Other irrelevant code is omitted }
Implement UserDetailsService interface
Implement the loadUserByUsername method, and modify the authentication information acquisition method as: obtain the permission information from the database.
/** * @author Lai Bingfeng bingfengdev@aliyun.com * @version 1.0 * @date 2020/8/28 22:16 */ @Service("userService") public class UserServiceImpl implements UserDetailsService { @Autowired private IUserDao userDao; @Autowired private IRoleDao roleDao; @Autowired private IPermissonDao permissonDao; @Override public 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; } }
Tip: the database operations and entity classes of users, roles and permissions are omitted here, which will not affect your understanding. Of course, the complete code download address is provided at the end of the article.
Custom 401 and 403 exception handling
Exceptions in Spring Security are mainly divided into two categories: authentication exceptions and authorization related exceptions. Moreover, the place where the exception is thrown is in the filter chain. If you use @ ControllerAdvice, there is no way to handle it.
Of course, an excellent framework like spring security certainly takes this issue into account.
The exceptionHandling() method provided by HttpSecurity in spring security is used to provide exception handling. This method constructs the ExceptionHandlingConfigurer exception handling configuration class.
Then, this class provides two interfaces for our custom exception handling:
- AuthenticationEntryPoint this class is used to uniformly handle AuthenticationException exceptions (403 exceptions)
- AccessDeniedHandler this class is used to uniformly handle AccessDeniedException exceptions (401 exceptions)
MyAuthenticationEntryPoint.java
/**401 exception handling * @author Lai Bingfeng bingfengdev@aliyun.com * @version 1.0 * @date 2020/9/3 22:08 */ public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public 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","The token has expired. Please login again"); ServletOutputStream out = response.getOutputStream(); String s = new ObjectMapper().writeValueAsString(map); byte[] bytes = s.getBytes(); out.write(bytes); } }
MyAccessDeniedHandler.java
/**403 exception handling * @author Lai Bingfeng bingfengdev@aliyun.com * @version 1.0 * @date 2020/9/3 22:11 */ public class MyAccessDeniedHandler implements AccessDeniedHandler { @Override public 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","You are not authorized to access this resource. If necessary, please contact the administrator for authorization"); ServletOutputStream out = response.getOutputStream(); String s = new ObjectMapper().writeValueAsString(map); byte[] bytes = s.getBytes(); out.write(bytes); } }
Add these two classes to the configuration of spring security
/**spring security Configuration class * @author Lai Bingfeng bingfengdev@aliyun.com * @version 1.0 * @date 2020/9/3 15:41 */ @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class SpringSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsService userService; @Autowired private AuthServerRsaKeyProperties properties; @Bean public BCryptPasswordEncoder myPasswordEncoder(){ return new BCryptPasswordEncoder(); } /**Configure custom filters * @author Lai Bingfeng bingfengdev@aliyun.com * @date 2020-09-03 15:53:45 * @param http * @version 1.0 */ @Override protected void configure(HttpSecurity http) throws Exception { //Other codes... //Add custom exception handling http.exceptionHandling().authenticationEntryPoint(new MyAuthenticationEntryPoint()); http.exceptionHandling().accessDeniedHandler(new MyAccessDeniedHandler()); //Other code 1 } }
At this step, you can run the startup class and test it first. In this paper, the product service module is also implemented first, and then tested intensively
Create subsystem module product service
Modify POM XML file
This step is almost the same as when we created the authentication service.
<?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>
Modify application YML profile
Here are mainly the configuration database information and the address information added to the public key
server: port: 8082 spring: datasource: url: jdbc:mysql://127.0.0.1:3306/security_authority?useSSL=false&serverTimezone=GMT username: root password: root1997 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: configuration: map-underscore-to-camel-case: true logging: level: pers.lbf: debug lbf: key: publicKeyPath: Your public key address
Create a configuration class that reads the public key
/**Read public key configuration class * @author Lai Bingfeng 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; @PostConstruct public void loadKey() throws Exception { publicKey = RsaUtils.getPublicKey(publicKeyPath); } @Override public 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; } }
Modify startup class
This step is the same as when creating an authentication server, such as adding public key configuration and mapper scanning
/** * @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); } }
copy
This step is mainly to copy UserAuthVo, custom verifier, custom exception handler and custom deserializer from the authentication service module. (the reason why it is not put into the common module is that it does not want to directly introduce the dependency of springBoot integration spring security into the common module)
Create sub module spring security configuration class
Here, you only need to modify the configuration of the authentication service module to remove the content of the user-defined authentication filter. The resource module is only responsible for verification, not authentication.
Create a test interface
/** * @author Lai Bingfeng 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 "Product information interface call succeeded!"; } }
Step 3: start the project and test it
Login (authentication) operation
Message prompt returned after successful login
And you can see the token in the request header
Login failure prompts "wrong user name or password"
Access resources
Carry a token to access resources, and have permission, and the token has not expired
Carry a token to access resources. But I don't have permission
Access without token (not logged in or authenticated)
Access resources with expired tokens
Write at the end
springBoot integrates security to realize rights management and authentication. The core of the distributed version (front and rear separated version) lies in three problems
- session is disabled. Where is the user information saved?
- How to authenticate visitors, or authenticate visitors according to token s?
- How to verify the visitor, or verify the visitor identity according to the token?
Basically, after we have solved the above three problems, springBoot integrates spring security to realize the permission management and authentication in the scenario of front-end and back-end separation (distributed).
Download method of code and sql script: wechat search and follow the official account [Java development practice], and reply to 20200904 to get the download link.