Writing in the front

In the previous article “springBoot integration of Spring Security permission management (single application version)- foundation in the early stage”, we introduce springBoot integration of Spring Security single application version, in this article, I will introduce springBoot integration with Spring Secury +JWT for single sign-on and permission management.

The authority management model involved in this paper is resource-based dynamic authority management. The database design tables are user, role, user_role, Permission, and ROLE_Permission.

In single sign-on, there are many solutions to store visitor information. If it is stored in the redis database as a key-value, the visitor token holds the key. When verifying the user’s identity, use the key in the visitor’s token to look for value in Redis. If no key is found, return “token expired” and let the visitor (re) authenticate. Demo in this paper encrypts the visitor’s information and returns it to the visitor in token. When the visitor carries the token to access the service, the service provider can directly decrypt and verify the token. Both implementations have advantages and disadvantages. You can also try adapting the visitor information stored in the demo in this article to redis. At the end of the article provides the complete code and SQL script download address.

Here’s what we need to know before we get started.

Single sign-on SSO

Single sign-on (SSO) is also called distributed authentication. In a project with multiple systems, users are authenticated once to access the trusted systems under the project.

Single sign-on process

I drew a flow chart for you

About the JWT

JWT, which stands for JSON Web Token, is an excellent distributed authentication solution.

JWT consists of three parts

  1. Header: mainly set some standard information, the signature part of the encoding format is declared in the header.
  2. Payload: The part of the token that holds valid information, such as user name, user role, expiration time, etc., but is not suitable for sensitive data, such as passwords, which can cause leakage.
  3. Signature: After encoding the header and payload in Base64, use “. Join, add salt, and then code using the type of code declared in the header to get the signature.

Security analysis of Token generated by JWT

To keep the token from being forged, make sure the signature is not tampered with. However, the header and payload of its signature are base64 encoded, which is indistinguishable from plaintext. So we had to fiddle with the salt. After asymmetrically encrypting the salt, we issue the token to the user.

RSA asymmetric encryption

  1. Basic principle: Generate two keys at the same time: the private key and the public key. The private key is stored in secret and the public key can be sent to the trusted client.

    • Public key encryption: Only the private key can be decrypted. Generally, the public key has multiple copies

    • Private key encryption: Only the public key can be decrypted. Generally, there is only one private key

  2. The advantages and disadvantages:

    • Advantages: Security, difficult to crack

    • Cons: Time consuming, but acceptable for security

Analysis of SpringSecurity+JWT+RSA distributed authentication

Through previous learning, we know that Spring Security is mainly based on the filter chain to do authentication, so how to build our single sign-on, the breakthrough lies in the authentication filter in Spring Security.

User authentication

In distributed projects, most of which are now designed with a back-end separation architecture, we need authentication parameters that can accept POST requests rather than traditional form submissions. Therefore, we need to modify the desired change UsernamePasswordAuthenticationFilter attemptAuthentication method in the filter and let it receives a request body.

For the analysis of the Spring Security authentication process, you can refer to my previous article “Analysis of the Spring Security Authentication Process — Post-Training”.

In addition, by default, the successfulAuthentication method simply puts the authentication information into the server’s session after being authenticated. In our distributed application, the front and back ends are separated and session is disabled. Therefore, we need to generate the token (payload with the necessary information to verify the user’s identity) and return it to the user after the authentication passes.

Identity check

By default, doFilterInternal BasicAuthenticationFilter a filter method to check whether the user login, is to see if there is a user information session. In distributed applications, we need to verify whether the token carried by the user is legitimate, and parse out the user information to SpringSecurity, so that subsequent authorization functions can be used normally.

Implementation steps

(By default, you have created the database)

Step 1: Create a springBoot project

This parent project does dependency versioning.

The pom.xml file is shown below


      
<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 - the 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 - the SNAPSHOT</common.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>pers.lbf</groupId>
                <artifactId>common</artifactId>
                <version>${common.version}</version>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-security</artifactId>
                <version>${springboot.version}</version>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
                <version>${springboot.version}</version>
            </dependency>
            <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
                <version>${mybatis.version}</version>
            </dependency>
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>${mysql.version}</version>
            </dependency>

            <! JWT jar 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>
            <! Json toolkits -->
            <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>
Copy the code

Step 2: Create three submodules

The Common module, as a public module, provides basic services, including token generation, RSA encryption key generation and use, Json serialization and deserialization.

The authentication-service module provides single sign-on (SSO) services (user authentication and authorization).

The product-Service module simulates a subsystem. It is responsible for providing interface calls and verifying user identity.

createcommonThe module module

##### Modify pom. XML to add dependencies such as JWT and JSON

pom.xml


      
<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 - the SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>common</artifactId>

    <dependencies>
        <! JWT 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>
        <! Json toolkits -->
        <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>
Copy the code
Create a JSON utility class
** JSON utility class *@authorLai handle being 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(a) {}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; }}}Copy the code
Create an RSA encryption utility class and generate a public key and key file

​ RsaUtils.java

/**RSA asymmetric encryption tool class *@authorLaifeng [email protected] *@version 1.0
 * @date2020/9/2 * /
public class RsaUtils {

    private static final int DEFAULT_KEY_SIZE = 2048;
    
    /** Read the public key from the file *@authorLaifeng [email protected] *@dateThe 2020-09-04 13:10:15 *@paramFilename Specifies the path to save the public key. The value is relative to classpath *@returnJava.security.publickey PublicKey object *@throws Exception
     * @version1.0 * /
    public static PublicKey getPublicKey(String filename) throws Exception {
       
        byte[] bytes = readFile(filename);
        return getPublicKey(bytes);
    }

    
    /** Read the key from the file *@authorLaifeng [email protected] *@dateThe 2020-09-04 13:12:01 *@paramFilename Specifies the path to save the private key, relative to classpath *@returnJava.security.privatekey PrivateKey object *@throws Exception
     * @version1.0 * /
    public static PrivateKey getPrivateKey(String filename) throws Exception {
        byte[] bytes = readFile(filename);
        return getPrivateKey(bytes);
        
    }

    / * * *@authorLaifeng [email protected] *@dateThe 2020-09-04 13:12:59 *@paramBytes Bytes of the public key *@returnJava.security.publickey PublicKey object *@throws Exception
     * @version1.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 the key *@authorLaifeng [email protected] *@dateThe 2020-09-04 13:14:02 *@paramBytes Bytes of the private key *@return java.security.PrivateKey
     * @throws Exception
     * @version1.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);
        
    }

    /** * Live the RSA public and private keys according to the ciphertext, and write the specified file *@authorLaifeng [email protected] *@dateThe 2020-09-04 13:14:02 *@paramPublicKeyFilename Public key file path *@paramPrivateKeyFilename Private key file path *@paramSecret Indicates the ciphertext for generating the 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
        byte[] publicKeyBytes = keyPair.getPublic().getEncoded();
        publicKeyBytes = Base64.getEncoder().encode(publicKeyBytes);
        writeFile(publicKeyFilename, publicKeyBytes);
        // Get the private key and write
        byte[] privateKeyBytes = keyPair.getPrivate().getEncoded();
        privateKeyBytes = Base64.getEncoder().encode(privateKeyBytes);
        writeFile(privateKeyFilename, privateKeyBytes);
    }

    /** Read the file *@authorLaifeng [email protected] *@dateThe 2020-09-04 13:15:37 *@param fileName
     * @return byte[]
     * @throws
     * @version1.0 * /
    private static byte[] readFile(String fileName) throws Exception {
        return Files.readAllBytes(new File(fileName).toPath());
       
    }

    /** Write file *@authorLaifeng [email protected] *@dateThe 2020-09-04 13:16:01 *@param destPath
     * @param bytes
     * @return void
     * @throws
     * @version1.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); }/** Privatize the constructor *@authorLaifeng [email protected] *@dateThe 2020-09-04 13:16:29 *@param
     * @return
     * @throws
     * @version1.0 * /
    private RsaUtils(a) {}}Copy the code

Generate the private key and public key files

/** * @author [email protected] * @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 [email protected] * @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); }}Copy the code

Private key file must be protected!!

Private key file must be protected!!

Private key file must be protected!!

(Important things say three times!!)

##### Create the Token payload entity class and the JWT utility classCopy the code
/** To facilitate the later acquisition of user information in the token, * encapsulates the payload part of the token into an object *@authorLaifeng [email protected] *@version 1.0
 * @date2020/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。。。
}
Copy the code

JwtUtils

/** Token utility class *@authorLaifeng [email protected] *@version 1.0
 * @date2020/9/2 "* /
public class JwtUtils {

    private static final String JWT_PAYLOAD_USER_KEY = "user";

    /** * Private key encryption token **@paramUserInfo Payload data *@paramPrivateKey private key *@paramExpire 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 **@paramUserInfo Payload data *@paramPrivateKey private key *@paramExpire 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 parse token **@paramToken Indicates the token * in the user request@paramPublicKey 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(a) {
        return new String(Base64.getEncoder().encode(UUID.randomUUID().toString().getBytes()));
    }

    /** * Get user information from token **@paramToken Token * in the user request@paramPublicKey public key *@returnUser 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 payload information in token **@paramToken Token * in the user request@paramPublicKey public key *@returnUser 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(a) {}}Copy the code

Once you’ve written the Common module, package it up and install it, and the next two services need to reference it.

Example Create the authentication service module authentication-service

The key point of the authentication service module is to customize user authentication filters and user validation filters and load them into Spring Security’s filter chain instead of the default ones.

##### Modify the pom. XML file and add related dependenciesCopy the code

pom.xml


      
<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 - the SNAPSHOT</version>
    </parent>
    <artifactId>authentication-service</artifactId>
    <version>1.0.0 - the 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>

Copy the code

The main dependencies added by this module are springBoot’s integration with Spring Security and database-related dependencies, as well as our Common module.

Modify the application.yml file

This step is to set the database connection information and the location information of public and private keys

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

Copy the code
Configure public and private key resolution
** Configuration classes for resolving public and private keys *@authorLai handle being 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 and private keys in the file * by@PostConstructThe decorated method is run when the server loads the Servlet, * and is executed by the server only once. PostConstruct is executed after the constructor and before the * init () method. *@authorLaifeng [email protected] *@dateThe 2020-09-03 12:07:35 *@throws Exception e
     * @version1.0 * /
    @PostConstruct
    public void loadKey(a) throws Exception {
        publicKey = RsaUtils.getPublicKey(publicKeyPath);
        privateKey = RsaUtils.getPrivateKey(privateKeyPath);

    }

    public String getPublicKeyPath(a) {
        return publicKeyPath;
    }

    public void setPublicKeyPath(String publicKeyPath) {
        this.publicKeyPath = publicKeyPath;
    }

    public String getPrivateKeyPath(a) {
        return privateKeyPath;
    }

    public void setPrivateKeyPath(String privateKeyPath) {
        this.privateKeyPath = privateKeyPath;
    }

    public PublicKey getPublicKey(a) {
        return publicKey;
    }

    public void setPublicKey(PublicKey publicKey) {
        this.publicKey = publicKey;
    }

    public PrivateKey getPrivateKey(a) {
        return privateKey;
    }

    public void setPrivateKey(PrivateKey privateKey) {
        this.privateKey = privateKey; }}Copy the code
Modify startup class, add token encryption and resolution configuration and Mapper scan
/ * * *@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); }}Copy the code
Create the user login object UserLoginVO

Instead of using the UserTO corresponding to the database table, we encapsulate the request parameters for the user login into an entity class.

/** User login request parameter object *@authorLaifeng [email protected] *@version 1.0
 * @date2020/9/3 laboureth * /
public class UserLoginVo implements Serializable {

    private String username;
    private String password;

    getter。。。
    settter。。。
}
Copy the code
Create the user credential object UserAuthVO

This object is mainly used to store the information in the token of the visitor after the successful authentication. We don’t store sensitive data like passwords here.

/** User credential object *@authorLaifeng [email protected] *@version 1.0
 * @date2020/9/3 justice * /
public class UserAuthVO implements Serializable {

    private String username;
    private List<SimpleGrantedAuthority> authorities;

   getter。。。
   setter。。。
}
Copy the code
Example Creating a custom authentication filter
/** Custom authentication filter *@authorLaifeng [email protected] *@version 1.0
 * @date2020/9/3 and * /
public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {

    /** * Authentication manager */

    private AuthenticationManager authenticationManager;

    private AuthServerRsaKeyProperties prop;

    /** construct injection *@authorLaifeng [email protected] *@dateThe 2020-09-03 12:17:54 *@paramAuthenticationManager authenticationManager for spring security *@paramProp Public and private key configuration classes *@version1.0 * /
    public TokenLoginFilter(AuthenticationManager authenticationManager, AuthServerRsaKeyProperties prop) {
        this.authenticationManager = authenticationManager;
        this.prop = prop;

    }


    /** Receives and parses user credentials, and returns JSON data *@authorLaifeng [email protected] *@dateThe 2020-09-03 12:19:29 *@param request req
     * @param response resp
     * @return Authentication
     * @version1.0 * /
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response){

        // Determine whether the request is POST and disable the GET request to submit data
        if (!"POST".equals(request.getMethod())) {
            throw new AuthenticationServiceException(
                    "POST request only");
        }


        // Convert json data to 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 newRuntimeException(e); }}/** This method is called upon successful authentication * after the user logs in successfully, the token is generated and the json data is returned to the front end *@authorLaifeng [email protected] *@dateThe 2020-09-03 13:00:23 *@param request
     * @param response
     * @param chain
     * @param authResult
     * @version1.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());

        // Create a token using JWT, private key encryption
        String token = JwtUtils.generateTokenExpireInMinutes(user,prop.getPrivateKey(),15);

        / / returns a token
       response.addHeader("Authorization"."Bearer"+token);

       // Successful login returns a JSON data message
        try {
            // Generate a 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 newRuntimeException(e); }}}Copy the code

At this point, you may start to find it difficult to understand, which requires a little understanding of Spring Security’s authentication process. You can read my previous article “Analysis of the Spring Security Authentication Process — Post-Training.”

Create a custom verification filter
/** Custom authenticator *@authorLaifeng [email protected] *@version 1.0
 * @date2020/9/3 15:02 * /
public class TokenVerifyFilter extends BasicAuthenticationFilter {

    private AuthServerRsaKeyProperties prop;

    public TokenVerifyFilter(AuthenticationManager authenticationManager, AuthServerRsaKeyProperties prop) {
        super(authenticationManager);
        this.prop = prop;
    }

    /** Filter requests *@authorLaifeng [email protected] *@dateThe 2020-09-03 15:07:27 *@param request
     * @param response
     * @param chain
     * @version1.0 * /
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException, AuthenticationException,ExpiredJwtException {

       // Determine whether the Authorization is included in the header of the request body
       String authorization = request.getHeader("Authorization");
       // Whether Bearer is included in the Authorization, direct return is not included
       if (authorization==null| |! authorization.startsWith("Bearer")){
           chain.doFilter(request, response);
           return;
       }

       UsernamePasswordAuthenticationToken token;
       try {
           // Parse the tokens generated by JWT to obtain permissions
            token = getAuthentication(authorization);

       }catch (ExpiredJwtException e){
          // e.printStackTrace();
           chain.doFilter(request, response);
           return;
       }

        // Write Authentication to SecurityContextHolder for later use
        SecurityContextHolder.getContext().setAuthentication(token);
        chain.doFilter(request, response);


    }



    /** Parse tokens generated by JWT *@authorLaifeng [email protected] *@dateThe 2020-09-03 15:21:04 *@param authorization auth
     * @return org.springframework.security.authentication.UsernamePasswordAuthenticationToken
     * @throws
     * @version1.0 * /
    public UsernamePasswordAuthenticationToken getAuthentication(String authorization) throws ExpiredJwtException{

        if (authorization == null) {
            return null;
        }

        Payload<UserAuthVO> payload;

            // Get the payload from the token
        payload = JwtUtils.getInfoFromToken(authorization.replace("Bearer".""), prop.getPublicKey(), UserAuthVO.class);



        // Get the current access object
        UserAuthVO userInfo = payload.getUserInfo();
        if (userInfo == null) {return null;
        }

        // Encapsulate the current access object and its permissions as tokens recognized by Spring Security
        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(userInfo,null,userInfo.getAuthorities());
        returntoken; }}Copy the code
Write the configuration classes for Spring Security

This step is mainly to complete the configuration of Spring Security. The only difference in integrating Spring ‘Security with a standalone application is that this step requires us to add our own custom filters for user authentication and validation, as well as disable sessions.

/** Spring Security configuration class *@authorLaifeng [email protected] *@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(a){
        return new BCryptPasswordEncoder();
    }


    /** Configure a custom filter *@authorLaifeng [email protected] *@dateThe 2020-09-03 15:53:45 *@param http
     * @version1.0 * /
    @Override
    protected void configure(HttpSecurity http) throws Exception {

        // Disable cross-domain protection instead of JWT
        http.csrf().disable();

        // A method that allows anonymous access
        http.authorizeRequests().antMatchers("/login").anonymous();
                // Others require authentication
                //.anyRequest().authenticated();

        // Add an authentication filter
        http.addFilter(new TokenLoginFilter(authenticationManager(),properties));

        // Add validation filter
        http.addFilter(new TokenVerifyFilter(authenticationManager(),properties));


        // When session is disabled, the front end separation is stateless
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);


    }



    /** Configure the password encryption policy *@authorLaifeng [email protected] *@dateThe 2020-09-03 15:50:46 *@param authenticationManagerBuilder
     * @version1.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"); }}Copy the code
Add a custom deserialization tool for GrantedAuthority type

Since our permission information was encrypted and stored in the token, we serialized and deserialized the authorities, and since Jackson did not support deserialization, we had to do it ourselves.

* * *@authorLai handle being 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()));
        }
        returngrantedAuthorities; }}Copy the code

Tag UserAuthVO

/** User credential object *@authorLaifeng [email protected] *@version 1.0
 * @date2020/9/3 justice * /
public class UserAuthVO implements Serializable {

    @JsonDeserialize(using = CustomAuthorityDeserializer.class)
    public void setAuthorities(List<SimpleGrantedAuthority> authorities) {
        this.authorities = authorities;
    }

   // Omit other extraneous code
}

Copy the code
Implement the UserDetailsService interface

Implement the loadUserByUsername method and change the method of obtaining authentication information to obtain permission information from the database.

/ * * *@authorLaifeng [email protected] *@version 1.0
 * @date2020/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);
        returnuser; }}Copy the code

** Note: ** about the user, role, permission database operations and their entity classes are omitted here, does not affect everyone’s understanding, of course, at the end of the article provided a complete code download address.

Custom 401 and 403 exception handling

Exceptions in Spring Security fall into two main categories: authentication exceptions and authorization-related exceptions. And where it throws an exception is in the filter chain, which you can’t handle if you use @ControllerAdvice.

Of course, a framework as good as Spring Security certainly takes this into account.

The HttpSecurity exceptionHandling() method in spring security is used to provide exceptionHandling. This method constructs the ExceptionHandlingConfigurer exception handling configuration class.

This class provides two interfaces for custom exception handling:

  • AuthenticationEntryPoint This class is used to uniformly handle AuthenticationException (403 exception)
  • AccessDeniedHandler This class is used to uniformly handle AccessDeniedException exceptions (401 exceptions)

MyAuthenticationEntryPoint.java

/**401 Exception handling *@authorLaifeng [email protected] *@version 1.0
 * @date2020/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"."Token expired please log in again");

        ServletOutputStream out = response.getOutputStream();
        String s = new ObjectMapper().writeValueAsString(map);
        byte[] bytes = s.getBytes(); out.write(bytes); }}Copy the code

MyAccessDeniedHandler.java

/**403 Exception handling *@authorLaifeng [email protected] *@version 1.0
 * @date2020/9/3 [* /
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"."Unauthorized access to this resource, please contact your administrator for authorization.");
        ServletOutputStream out = response.getOutputStream();
        String s = new ObjectMapper().writeValueAsString(map);
        byte[] bytes = s.getBytes(); out.write(bytes); }}Copy the code

Add these two classes to your Spring Security configuration

/** Spring Security configuration class *@authorLaifeng [email protected] *@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(a){
        return new BCryptPasswordEncoder();
    }


    /** Configure a custom filter *@authorLaifeng [email protected] *@dateThe 2020-09-03 15:53:45 *@param http
     * @version1.0 * /
    @Override
    protected void configure(HttpSecurity http) throws Exception {

        // Other code...
       
        // Add custom exception handling
        http.exceptionHandling().authenticationEntryPoint(new MyAuthenticationEntryPoint());
        http.exceptionHandling().accessDeniedHandler(new MyAccessDeniedHandler());

        // Other code 1}}Copy the code

So at this point you can run your startup class and test it out. In this paper, the product-Service module is also implemented in the first centralized test

Create subsystem module product-service

Modify the POM.xml file

This step is the same as when we created the authentication service.


      
<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 - the SNAPSHOT</version>
    </parent>

    <artifactId>product-service</artifactId>
    <version>1.0.0 - the 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>

Copy the code
Modify the application.yml configuration file

This section describes how to configure database information and add public key addresses

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

Copy the code
Create a configuration class that reads public keys
/** Read public key configuration class *@authorLaifeng [email protected] *@version 1.0
 * @date2020/9/4 10:05 * /
@ConfigurationProperties(prefix = "lbf.key")
@ConstructorBinding
public class ProductRsaKeyProperties {

    private String publicKeyPath;
    private PublicKey publicKey;

    @PostConstruct
    public void loadKey(a) throws Exception {
        publicKey = RsaUtils.getPublicKey(publicKeyPath);
    }

    @Override
    public String toString(a) {
        return "ProductRsaKeyProperties{" +
                "pubKeyPath='" + publicKeyPath + '\' ' +
                ", publicKey=" + publicKey +
                '} ';
    }

    public String getPublicKeyPath(a) {
        return publicKeyPath;
    }

    public void setPublicKeyPath(String publicKeyPath) {
        this.publicKeyPath = publicKeyPath;
    }

    public PublicKey getPublicKey(a) {
        return publicKey;
    }

    public void setPublicKey(PublicKey publicKey) {
        this.publicKey = publicKey; }}Copy the code
Modifying the 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 the code
copy

This step involves copying the UserAuthVo, custom validator, custom exception handler, and custom deserializer from the authentication service module. (It is not put into the common module because. Don’t want to introduce springBoot directly into the Common module to integrate Spring Security dependencies.

Create the spring Security configuration class for the submodule

You only need to modify the configuration of the authentication service module to remove the user-defined authentication filter. The resource module is only responsible for verification, not authentication.

Create a test interface
/ * * *@authorLaifeng [email protected] *@version 1.0
 * @date2020/8/27 20:02 * /
@RestController
@RequestMapping("/product")
public class ProductController {


    @GetMapping("/get")
    @PreAuthorize("hasAuthority('product:get')")
    public String get(a) {
        return "Product info interface call successful!"; }}Copy the code

Step 3: Start the project and test it

Login (authentication) operation

A message is displayed indicating successful login

The token can also be seen in the request header

Login failure prompt “wrong username or password”

Access to the resource

You have permission to access resources with a token and the token has not expired

Use a token to access resources. But you don’t have access

Access without token (not logged in, not authenticated)

Access resources with expired tokens

Write in the last

The core of springBoot’s integrated Security implementation of permission management and authentication distributed version (front-end and back-end separate version) lies in three issues

  1. If session is disabled, where is user information stored?

  2. How to authenticate visitors, or authenticate visitors according to token?

  3. How to verify the visitor’s identity according to the token?

Basically we solved the above three problems, springBoot integration of Spring Security to achieve the front and back end separation (distributed) scenario of permission management and authentication problems we can say that is basically solved.

** Code and SQL script download method: ** wechat search public account [Java development practice], reply 20200904 to get the download link.