springBoot integrates spring security+JWT to realize the separation of single sign on and permission management -- the middle stage of foundation construction

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

  1. Header: it mainly sets some specification information, and the coding format of the signature part is declared in the header.
  2. 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.
  3. 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

  1. 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
  2. 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

  1. session is disabled. Where is the user information saved?
  2. How to authenticate visitors, or authenticate visitors according to token s?
  3. 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.

Tags: Java spring-security

Posted by Brusca on Wed, 11 May 2022 22:28:13 +0300