SpringSecurity Framework [Detailed]

SpringSecurity

Source Video

Article Directory

1. Overview

Spring Security is a security management framework in the Spring family. It provides richer functionality and community resources than Shiro, another security framework.

Spring Security is a powerful and highly customizable authentication and access control framework. It is the actual standard used to protect Spring-based applications;

Spring Security is a framework for providing authentication and authorization for Java applications. As with all Spring projects, the real power of Spring Security is that it can be easily extended to meet custom requirements.

In the Java ecology, there are Spring Security and Apache Shiro Two Security frameworks to accomplish authentication and authorization functions.

Let's start with Spring Security. Their official description of themselves is as follows:

Spring Security is a powerful and highly customizable authentication and access-control framework. It is the de-facto standard for securing Spring-based applications.

Spring Security is a powerful and highly customizable authentication and access control framework. It is the de facto standard for protecting Spring-based applications.

Spring Security is a framework that focuses on providing both authentication and authorization to Java applications. Like all Spring projects, the real power of Spring Security is found in how easily it can be extended to meet custom requirementsSpring

Security is a framework that focuses on providing authentication and authorization for Java applications. As with all Spring projects, the real power of Spring Security lies in how easily it can be extended to meet custom needs

Common Web applications require authentication and authorization.

Authentication: Verify that the user currently accessing the system is a user of the system, and confirm who the user is.

Authorization: Once authenticated, determine whether the current user has permission to perform an operation

Authentication and authorization are the core functions of SpringSecurity as a security framework.

2. Selection of Spring Security, Apache Shiro

2.1,Shiro

First Shiro Compared to Spring Security, Shiro maintains powerful functionality while still having great advantages in simplicity and flexibility.

Shiro is a powerful and flexible open source security framework that handles authentication, authorization, session management, and password encryption very clearly. Here are its features:

  1. An easy-to-understand Java Security API;

  2. Simple authentication (login), support for multiple data sources (LDAP, JDBC, Kerberos, Active Directory, etc.);

  3. Simple signing rights (access control) for roles, supporting fine-grained signing rights;

  4. Supports one-level caching to improve application performance;

  5. Built-in POJO-based enterprise session management for both Web and non-Web environments;

  6. Heterogeneous client session access;

  7. Very simple encryption API;

  8. It can run independently without being bound to any frame or container.

    Four core functions of Shiro: Authentication,Authorization,Cryptography,Session Management

Four core functions are introduced:

  1. Authentication: Authentication/login to verify that the user has the appropriate identity;
  2. Authorization: Authorization, or privilege verification, verifies that an authenticated user has a privilege; That is, to determine whether a user can do something, such as verifying that a user has a role. Or fine-grained validation of whether a user has a certain privilege on a resource;
  3. Session Manager: Session management, which means that a user logs in to a session and all its information is in the session before exiting. Sessions can be in a normal JavaSE environment or a Web environment.
  4. Cryptography: Encrypt, protect the security of data, such as password encryption stored in the database instead of plain text storage;

Shiro architecture

Shiro has three core components: Subject, SecurityManager and Realms.

  1. Subject: The principal, you can see that the principal can be any user that can interact with the application;
  2. SecurityManager: Equivalent to Dispatcher Servlet in Spring MVC or FilterDispatcher in Struts2; Is Shiro's heart; All specific interactions are controlled through the SecurityManager; It manages all Subject s and is responsible for authentication and authorization, as well as session and cache management.
  3. Realm: Domain, where Shiro obtains security data (such as users, roles, permissions) from Realm, that is, to verify the identity of a user, SecurityManager needs to obtain the corresponding user from Realm for comparison to determine if the identity is legal; User roles/privileges from Realm are also required to verify whether the user can operate. Realm can be thought of as a DataSource, a secure data source.

Advantages of 2.1.1, shiro

  • shiro's code is easier to read and easier to use;
  • shiro can be used in non-web environments, run independently of any framework or container;

2.1.2, Shortcomings of shiro

  • Authorizing third-party logins requires manual implementation;

2.2,Spring Security

Apart from not being able to detach from Spring, Shiro has all of its capabilities. Spring Security also supports Oauth, OpenID, and Shiro needs to implement it manually. Spring Security has finer-grained permissions, since Spring Security is a Spring family.

The Spring Security general process is:

  1. When a user logs in, the front end transmits the user name and password information entered by the user to the background, and the background encapsulates it with a class object, usually using the class UsernamePasswordAuthenticationToken.
  2. The program is responsible for validating this class of object. The authentication method is to call Service to retrieve the user information from the database to the instance of the entity class based on username, compare the passwords of the two, and if the password is correct, log on successfully. At the same time, put the class object containing the user's user name, password, permissions, etc. into the SecurityContextHolder (Security Context Container, similar to Session).
  3. When a user accesses a resource, he or she first determines whether it is a restricted resource. If so, also determine if you are not currently logged in, if not, skip to the login page.
  4. If a user is already logged in and accesses a restricted resource, the program uses the url to go to the database and retrieve all the accessible roles corresponding to that resource, then compares all the roles of the current user to determine if the user can access it (in this case, related to permissions).

Advantages of 2.2.1, spring-security

  • spring-security integrates spring well and is more convenient to use.
  • Support from a stronger spring community;
  • Supports third-party oauth authorization, official website: spring-security-oauth

3. Quick Start

3.1. Equipment Work

Let's create an empty project first

Create a normal maven project in the project

Change this plain maven to the SpringBoot project

1. Add Dependency

  <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.4</version>
    </parent>

    <dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>

    </dependencies>

2. Create Startup Class

@SpringBootApplication
public class IntroductionSpringSecurity {


    public static void main(String[] args) {
        SpringApplication.run(IntroductionSpringSecurity.class,args);
    }

}

3. Create Controller

@RestController
public class HelloController {


    @RequestMapping("/hello")
    public String hello(){
        return "World Hello";
    }

}

Test access:

4. Importing SpringSecurity Dependencies

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

Restart the test:

We can see that when we access our interface, we automatically jump to a SpringSecurity default landing page

At this point we need to log in to access it, and we can see that the console has a string of strings, which is actually the password that SpringSecurity initialization generated for me

Default user name:

Enter your user name and password and log in again

Success.

4. Certification

4.1. Login Process Check

4.2. Principles of introductory cases

Front-end and back-end authentication process:

  1. UsernamePasswordAuthenticationFilter: The main processing class for our most common user name and password authentication methods, constructs a UsernamePasswordAuthenticationToken object implementation class that encapsulates request information as Authentication

  2. Authentication interface: Encapsulates user-related information.

  3. AuthenticationManager Interface: Defines the method of authenticating Authentication, which is the core interface related to authentication and the starting point for initiating authentication, because in actual demand, we may allow users to log in using username+password, while allowing users to log in using mailbox+password, mobile phone number+password, or even, may allow users to log in using fingerprint (something like that?) So AuthenticationManager does not directly authenticate. ProviderManager, a common implementation class of AuthenticationManager interface, maintains a List of authentication methods inside it. In fact, this is a Delegate application. That is, there is always only one certification entry for the core: AuthenticationManager

    AuthenticationManager,ProviderManager ,AuthenticationProvider...

    Username Password Authentication Token, Mailbox+Password, Mobile Phone+Password Logon corresponds to three Authentication Providers

  4. DaoAuthenticationProvider: A certification service provider that resolves and certifies UsernamePasswordAuthenticationToken, corresponding to the above login methods.

  5. UserDetailsService interface: Spring Security passes the username filled in by the front end to UserDetailService.loadByUserName method. We just need to find the user information from the database based on the user name and return it to SpringSecurity as an implementation class encapsulated as UserDetails. We don't need to do password matching themselves. Password matching is handled by SpringSecurity.

  6. UserDetails interface: Provides core user information. User information processed by UserDetailsService based on user name is encapsulated and returned as a UserDetails object. These information are then encapsulated in the Authentication object.

UsernamePasswordAuthenticationFilter: The main processing class for our most common user name and password authentication methods, constructs a UsernamePasswordAuthenticationToken object implementation class that encapsulates request information as Authentication

Basic AuthenticationFilter...: Authentications encapsulated in the implementation class UsernamePasswordAuthenticationToken for login logic processing

AuthenticationManager

AuthenticationProvider

...

The UsernamePasswordAuthentication Token object is actually an implementation of Authentication that encapsulates the authentication information we need. AuthenticationManager is then called. This class doesn't actually validate our information. The logic of information validation is in Authentication Provider. Manager's role is to manage Providers by traversing through for loops (because different login logins are different, such as form login, third-party login (qq q login, mailbox login...). In other words, different providers support different Auhentications. Call the DaoAuthenticationProvider in AuthenticationManager. The DaoAuthenticationProvider inherits the AbstractUserDetailsAuthenticationProvider and therefore obtains the authenticate method to validate it.

[]: https://zhuanlan.zhihu.com/p/201029977

ExceptionTranslationFilter: Mainly used to handle exceptions to AuthenticationException and Acess DeniedException

FilterSecurityInterceptor: Get the permission configuration corresponding to the current request**, ** Call the access controller for authentication

4.3. Official Start

Sign in:

1. Custom login interface

Call ProviderManager's method to authenticate if authentication passes jwt generation

Store user information in redis

2. Customize UserDetailsService

Query the database in this implementation class

Verification:

1. Define Jwt authentication filters

Get token

Parse token to get the userid

Get user information from redis

Save in SecurityContextHolder

4.3.1 Preparations

Rebuild a new generic maven project

1. Add Dependency

  <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.4</version>
    </parent>

    <dependencies>


<!--        redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

<!--        json-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.79</version>
        </dependency>

<!--        jwt-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>

<!--        mysql-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>


        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

<!--       mybatis-plus-boot-starter-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.3</version>
        </dependency>



        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>

    </dependencies>

2. Create utils packages in the primary boot class sibling directory

Add Redis-related configurations

FastJsonRedisSerializer

package com.qx.utils;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.type.TypeFactory;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
import com.alibaba.fastjson.parser.ParserConfig;
import org.springframework.util.Assert;
import java.nio.charset.Charset;

/**
 * Redis Serialization using FastJson
 */
public class FastJsonRedisSerializer<T> implements RedisSerializer<T>
{

    public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");

    private Class<T> clazz;

    static
    {
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
    }

    public FastJsonRedisSerializer(Class<T> clazz)
    {
        super();
        this.clazz = clazz;
    }

    @Override
    public byte[] serialize(T t) throws SerializationException
    {
        if (t == null)
        {
            return new byte[0];
        }
        return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
    }

    @Override
    public T deserialize(byte[] bytes) throws SerializationException
    {
        if (bytes == null || bytes.length <= 0)
        {
            return null;
        }
        String str = new String(bytes, DEFAULT_CHARSET);

        return JSON.parseObject(str, clazz);
    }


    protected JavaType getJavaType(Class<?> clazz)
    {
        return TypeFactory.defaultInstance().constructType(clazz);
    }
}

3. Create a config package in the primary boot class sibling directory

RedisConfig

package com.qx.config;


import com.qx.utils.FastJsonRedisSerializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {

    @Bean
    @SuppressWarnings(value = { "unchecked", "rawtypes" })
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory)
    {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);

        FastJsonRedisSerializer serializer = new FastJsonRedisSerializer(Object.class);

        // Use StringRedisSerializer to serialize and deserialize the key value of redis
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(serializer);

        // Hash's key is also serialized using String RedisSerializer
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(serializer);

        template.afterPropertiesSet();
        return template;
    }
}

4. Set up a controller package in the same directory as the main boot class

Response class ResponseResult

package com.qx.domain;

import com.fasterxml.jackson.annotation.JsonInclude;

@JsonInclude(JsonInclude.Include.NON_NULL)
public class ResponseResult<T> {
    /**
     * Status Code
     */
    private Integer code;
    /**
     * Prompt message, if there is an error, the front end can get the field to prompt
     */
    private String msg;
    /**
     * Queried result data,
     */
    private T data;

    public ResponseResult(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public ResponseResult(Integer code, T data) {
        this.code = code;
        this.data = data;
    }

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }

    public ResponseResult(Integer code, String msg, T data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }
}

5. Place the tool class in the utils package

JwtUtil

package com.qx.utils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
import java.util.Date;
import java.util.UUID;

/**
 * JWT Tool class
 */
public class JwtUtil {

    //Valid for
    public static final Long JWT_TTL = 60 * 60 *1000L;// 60 * 60 * 1000 hours
    //Set Secret Key Clear Text
    public static final String JWT_KEY = "qx";

    public static String getUUID(){
        String token = UUID.randomUUID().toString().replaceAll("-", "");
        return token;
    }
    
    /**
     * Generate JTW JWT encryption
     * @param subject token Data to be stored in (json format)
     * @return
     */
    public static String createJWT(String subject) {
        JwtBuilder builder = getJwtBuilder(subject, null, getUUID());// Set expiration time
        return builder.compact();
    }

    /**
     * Generate JTW JWT encryption
     * @param subject token Data to be stored in (json format)
     * @param ttlMillis token timeout
     * @return
     */
    public static String createJWT(String subject, Long ttlMillis) {
        JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());// Set expiration time
        return builder.compact();
    }


    /**
     * Create token jwt encryption
     * @param id
     * @param subject
     * @param ttlMillis
     * @return
     */
    public static String createJWT(String id, String subject, Long ttlMillis) {
        JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);// Set expiration time
        return builder.compact();
    }

    private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        SecretKey secretKey = generalKey();
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);
        if(ttlMillis==null){
            ttlMillis=JwtUtil.JWT_TTL;
        }
        long expMillis = nowMillis + ttlMillis;
        Date expDate = new Date(expMillis);
        return Jwts.builder()
                .setId(uuid)              //Unique ID
                .setSubject(subject)   // Themes can be JSON data
                .setIssuer("sg")     // Issuer
                .setIssuedAt(now)      // Time filed
                .signWith(signatureAlgorithm, secretKey) //Sign using HS256 symmetric encryption algorithm, the second parameter is the key
                .setExpiration(expDate);
    }



    public static void main(String[] args) throws Exception {
        //jwt encryption
        String jwt = createJWT("123456");

        //jwt decryption
        Claims claims = parseJWT(jwt);
        String subject = claims.getSubject();



        System.out.println(subject);
        System.out.println(jwt);
    }

    /**
     * Generate encrypted secret key
     * @return
     */
    public static SecretKey generalKey() {
        byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
        SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
        return key;
    }
    
    /**
     * jwt Decrypt
     *
     * @param jwt
     * @return
     * @throws Exception
     */
    public static Claims parseJWT(String jwt) throws Exception {
        SecretKey secretKey = generalKey();
        return Jwts.parser()
                .setSigningKey(secretKey)
                .parseClaimsJws(jwt)
                .getBody();
    }


}

RedisCache

package com.qx.utils;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundSetOperations;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;

import java.util.*;
import java.util.concurrent.TimeUnit;

@SuppressWarnings(value = { "unchecked", "rawtypes" })
@Component
public class RedisCache
{
    @Autowired
    public RedisTemplate redisTemplate;

    /**
     * Cache basic objects, Integer, String, entity classes, etc.
     *
     * @param key Cached Key Values
     * @param value Cached value
     */
    public <T> void setCacheObject(final String key, final T value)
    {
        redisTemplate.opsForValue().set(key, value);
    }

    /**
     * Cache basic objects, Integer, String, entity classes, etc.
     *
     * @param key Cached Key Values
     * @param value Cached value
     * @param timeout time
     * @param timeUnit Time granularity
     */
    public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit)
    {
        redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
    }

    /**
     * Set valid time
     *
     * @param key Redis key
     * @param timeout timeout
     * @return true=Setup succeeded; false = setup failure
     */
    public boolean expire(final String key, final long timeout)
    {
        return expire(key, timeout, TimeUnit.SECONDS);
    }

    /**
     * Set valid time
     *
     * @param key Redis key
     * @param timeout timeout
     * @param unit Unit of time
     * @return true=Setup succeeded; false = setup failure
     */
    public boolean expire(final String key, final long timeout, final TimeUnit unit)
    {
        return redisTemplate.expire(key, timeout, unit);
    }

    /**
     * Gets the basic object of the cache.
     *
     * @param key Cache key value
     * @return Cache data corresponding to key values
     */
    public <T> T getCacheObject(final String key)
    {
        ValueOperations<String, T> operation = redisTemplate.opsForValue();
        return operation.get(key);
    }

    /**
     * Delete a single object
     *
     * @param key
     */
    public boolean deleteObject(final String key)
    {
        return redisTemplate.delete(key);
    }

    /**
     * Delete Collection Object
     *
     * @param collection Multiple Objects
     * @return
     */
    public long deleteObject(final Collection collection)
    {
        return redisTemplate.delete(collection);
    }

    /**
     * Cache List Data
     *
     * @param key Cached Key Values
     * @param dataList List data to be cached
     * @return Cached Objects
     */
    public <T> long setCacheList(final String key, final List<T> dataList)
    {
        Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
        return count == null ? 0 : count;
    }

    /**
     * Get cached list object
     *
     * @param key Cached Key Values
     * @return Cache data corresponding to key values
     */
    public <T> List<T> getCacheList(final String key)
    {
        return redisTemplate.opsForList().range(key, 0, -1);
    }

    /**
     * Cache Set
     *
     * @param key Cache key value
     * @param dataSet Cached data
     * @return Objects caching data
     */
    public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet)
    {
        BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
        Iterator<T> it = dataSet.iterator();
        while (it.hasNext())
        {
            setOperation.add(it.next());
        }
        return setOperation;
    }

    /**
     * Get Cached set
     *
     * @param key
     * @return
     */
    public <T> Set<T> getCacheSet(final String key)
    {
        return redisTemplate.opsForSet().members(key);
    }

    /**
     * Cache Map
     *
     * @param key
     * @param dataMap
     */
    public <T> void setCacheMap(final String key, final Map<String, T> dataMap)
    {
        if (dataMap != null) {
            redisTemplate.opsForHash().putAll(key, dataMap);
        }
    }

    /**
     * Get Cached Map s
     *
     * @param key
     * @return
     */
    public <T> Map<String, T> getCacheMap(final String key)
    {
        return redisTemplate.opsForHash().entries(key);
    }

    /**
     * Store data in Hash
     *
     * @param key Redis key
     * @param hKey Hash key
     * @param value value
     */
    public <T> void setCacheMapValue(final String key, final String hKey, final T value)
    {
        redisTemplate.opsForHash().put(key, hKey, value);
    }

    /**
     * Get data in Hash
     *
     * @param key Redis key
     * @param hKey Hash key
     * @return Hash Objects in
     */
    public <T> T getCacheMapValue(final String key, final String hKey)
    {
        HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();
        return opsForHash.get(key, hKey);
    }

    /**
     * Delete data from Hash
     * 
     * @param key
     * @param hkey
     */
    public void delCacheMapValue(final String key, final String hkey)
    {
        HashOperations hashOperations = redisTemplate.opsForHash();
        hashOperations.delete(key, hkey);
    }

    /**
     * Getting data from multiple Hash es
     *
     * @param key Redis key
     * @param hKeys Hash keyset
     * @return Hash Object Collection
     */
    public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys)
    {
        return redisTemplate.opsForHash().multiGet(key, hKeys);
    }

    /**
     * Get a list of cached basic objects
     *
     * @param pattern String Prefix
     * @return Object List
     */
    public Collection<String> keys(final String pattern)
    {
        return redisTemplate.keys(pattern);
    }
}

WebUtils

package com.qx.utils;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundSetOperations;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;

import java.util.*;
import java.util.concurrent.TimeUnit;

@SuppressWarnings(value = { "unchecked", "rawtypes" })
@Component
public class RedisCache
{
    @Autowired
    public RedisTemplate redisTemplate;

    /**
     * Cache basic objects, Integer, String, entity classes, etc.
     *
     * @param key Cached Key Values
     * @param value Cached value
     */
    public <T> void setCacheObject(final String key, final T value)
    {
        redisTemplate.opsForValue().set(key, value);
    }

    /**
     * Cache basic objects, Integer, String, entity classes, etc.
     *
     * @param key Cached Key Values
     * @param value Cached value
     * @param timeout time
     * @param timeUnit Time granularity
     */
    public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit)
    {
        redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
    }

    /**
     * Set valid time
     *
     * @param key Redis key
     * @param timeout timeout
     * @return true=Setup succeeded; false = setup failure
     */
    public boolean expire(final String key, final long timeout)
    {
        return expire(key, timeout, TimeUnit.SECONDS);
    }

    /**
     * Set valid time
     *
     * @param key Redis key
     * @param timeout timeout
     * @param unit Unit of time
     * @return true=Setup succeeded; false = setup failure
     */
    public boolean expire(final String key, final long timeout, final TimeUnit unit)
    {
        return redisTemplate.expire(key, timeout, unit);
    }

    /**
     * Gets the basic object of the cache.
     *
     * @param key Cache key value
     * @return Cache data corresponding to key values
     */
    public <T> T getCacheObject(final String key)
    {
        ValueOperations<String, T> operation = redisTemplate.opsForValue();
        return operation.get(key);
    }

    /**
     * Delete a single object
     *
     * @param key
     */
    public boolean deleteObject(final String key)
    {
        return redisTemplate.delete(key);
    }

    /**
     * Delete Collection Object
     *
     * @param collection Multiple Objects
     * @return
     */
    public long deleteObject(final Collection collection)
    {
        return redisTemplate.delete(collection);
    }

    /**
     * Cache List Data
     *
     * @param key Cached Key Values
     * @param dataList List data to be cached
     * @return Cached Objects
     */
    public <T> long setCacheList(final String key, final List<T> dataList)
    {
        Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
        return count == null ? 0 : count;
    }

    /**
     * Get cached list object
     *
     * @param key Cached Key Values
     * @return Cache data corresponding to key values
     */
    public <T> List<T> getCacheList(final String key)
    {
        return redisTemplate.opsForList().range(key, 0, -1);
    }

    /**
     * Cache Set
     *
     * @param key Cache key value
     * @param dataSet Cached data
     * @return Objects caching data
     */
    public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet)
    {
        BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
        Iterator<T> it = dataSet.iterator();
        while (it.hasNext())
        {
            setOperation.add(it.next());
        }
        return setOperation;
    }

    /**
     * Get Cached set
     *
     * @param key
     * @return
     */
    public <T> Set<T> getCacheSet(final String key)
    {
        return redisTemplate.opsForSet().members(key);
    }

    /**
     * Cache Map
     *
     * @param key
     * @param dataMap
     */
    public <T> void setCacheMap(final String key, final Map<String, T> dataMap)
    {
        if (dataMap != null) {
            redisTemplate.opsForHash().putAll(key, dataMap);
        }
    }

    /**
     * Get Cached Map s
     *
     * @param key
     * @return
     */
    public <T> Map<String, T> getCacheMap(final String key)
    {
        return redisTemplate.opsForHash().entries(key);
    }

    /**
     * Store data in Hash
     *
     * @param key Redis key
     * @param hKey Hash key
     * @param value value
     */
    public <T> void setCacheMapValue(final String key, final String hKey, final T value)
    {
        redisTemplate.opsForHash().put(key, hKey, value);
    }

    /**
     * Get data in Hash
     *
     * @param key Redis key
     * @param hKey Hash key
     * @return Hash Objects in
     */
    public <T> T getCacheMapValue(final String key, final String hKey)
    {
        HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();
        return opsForHash.get(key, hKey);
    }

    /**
     * Delete data from Hash
     * 
     * @param key
     * @param hkey
     */
    public void delCacheMapValue(final String key, final String hkey)
    {
        HashOperations hashOperations = redisTemplate.opsForHash();
        hashOperations.delete(key, hkey);
    }

    /**
     * Getting data from multiple Hash es
     *
     * @param key Redis key
     * @param hKeys Hash keyset
     * @return Hash Object Collection
     */
    public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys)
    {
        return redisTemplate.opsForHash().multiGet(key, hKeys);
    }

    /**
     * Get a list of cached basic objects
     *
     * @param pattern String Prefix
     * @return Object List
     */
    public Collection<String> keys(final String pattern)
    {
        return redisTemplate.keys(pattern);
    }
}

6. Set up entity classes

package com.qx.entity;

import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;
import java.util.Date;


/**
 * User table entity class
 *
 *
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("sys_user")
public class User implements Serializable {
    private static final long serialVersionUID = -40356785423868312L;
    
    /**
    * Primary key
    */
    @TableId
    private Long id;
    /**
    * User name
    */
    private String userName;
    /**
    * Nickname?
    */
    private String nickName;
    /**
    * Password
    */
    private String password;
    /**
    * Account status (0 normal 1 deactivated)
    */
    private String status;
    /**
    * mailbox
    */
    private String email;
    /**
    * Cell-phone number
    */
    private String phonenumber;
    /**
    * User gender (0 men, 1 woman, 2 unknown)
    */
    private String sex;
    /**
    * Head portrait
    */
    private String avatar;
    /**
    * User type (0 administrator, 1 normal user)
    */
    private String userType;
    /**
    * Creator's user id
    */
    private Long createBy;
    /**
    * Creation Time
    */
    private Date createTime;
    /**
    * Update Person
    */
    private Long updateBy;
    /**
    * Update Time
    */
    private Date updateTime;
    /**
    * Delete flag (0 for not deleted, 1 for deleted)
    */
    private Integer delFlag;
}

4.3.2, Implementation

From our previous analysis, we know that we can customize a UserDetailsService to allow SpringSecurity to use our UserDetailsService. Our own UserDetailsServices can query the database for user names and passwords.

Dead work

Create a user table with the following statement

CREATE TABLE `sys_user` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'Primary key',
  `user_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT 'User name',
  `nick_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT 'Nickname',
  `password` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT 'Password',
  `status` char(1) DEFAULT '0' COMMENT 'Account status (0 normal 1 deactivated))',
  `email` varchar(64) DEFAULT NULL COMMENT 'mailbox',
  `phonenumber` varchar(32) DEFAULT NULL COMMENT 'Cell-phone number',
  `sex` char(1) DEFAULT NULL COMMENT 'User gender (0 men, 1 woman, 2 unknown))',
  `avatar` varchar(128) DEFAULT NULL COMMENT 'Head portrait',
  `user_type` char(1) NOT NULL DEFAULT '1' COMMENT 'User type ( O Administrator, 1 Ordinary User)',
  `create_by` bigint DEFAULT NULL COMMENT 'Creator's user id',
  `create_time` datetime DEFAULT NULL COMMENT 'Creation Time',
  `update_by` bigint DEFAULT NULL COMMENT 'Update Person',
  `update_time` datetime DEFAULT NULL COMMENT 'Update Time',
  `del_flag` int DEFAULT '0' COMMENT 'Delete flag ( O Delegate not deleted, 1 delegate deleted)',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='User table';

Configure database information

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/qx_security?characterEncoding=utf-8&serverTimezone=UTC
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver
server:
  port: 8888

Define Mapper Interface

@Mapper
@Repository
public interface UserMapper extends BaseMapper<User> {
}

Test if MP works

/**
 * @author : k
 * @Date : 2022/3/23
 * @Desc :
 */

@SpringBootTest
public class UserMapperTests {

    @Autowired
    private UserMapper userMapper;

    @Test
    public void testUserMapper(){
        List<User> userList = userMapper.selectList(null);
        for (User user : userList) {
            System.out.println(user);
        }
    }


}

4.3.3, Core Code Implementation

Create a class that implements the UserDetailsService interface and override its methods. Add a user name to query user information from the database

package com.qx.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.qx.entity.LoginUser;
import com.qx.entity.User;
import com.qx.mapper.MenuMapper;
import com.qx.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Objects;

/**
 * @author : k
 * @Date : 2022/3/23
 * @Desc :
 */
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;


        //Implement the UserDetailsService interface, override the UserDetails method, and query custom user information from data
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        //(Authentication, which verifies the existence of the user) Query user information
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(User::getUserName,username);
        User user = userMapper.selectOne(queryWrapper);
        //If no user is queried
        if (Objects.isNull(user)){
            throw new RuntimeException("User name or password error");
        }


        //TODO (Authorization, i.e. Query what permissions a user has) Query the corresponding user information


        //Encapsulate data as UserDetails to return
        return new LoginUser(user);
    }
}

Because the return value of the UserDetailsService method is of type UserDetails, you need to define a class that implements the interface and encapsulates user information in it.

@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginUser implements UserDetails {

    private User user;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getUserName();
    }

    //Is it not expired
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    //Is it unlocked
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    //Does the voucher not expire
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    //Is Available
    @Override
    public boolean isEnabled() {
        return true;
    }
}

Note: If you want to test, you need to write user data to the user table, and if you want the user's password to be stored in clear text, you need to add {noop} before the password.

4.3.3.1, Password Encrypted Storage

We will not store the password plain text in the database in the actual project.

PasswordEncoder, which is used by default, requires that the password format in the database be: {id}password. It determines how passwords are encrypted based on the ID. But we don't normally do that. So you need to replace PasswordEncoder.

We typically use the BCryptPasswordEncoder provided to us by SpringSecurity.

Simply inject the BCryptPasswordEncoder object into the Spring container, and SpringSecurity will use that PasswordEncoder for password verification.

We can define a configuration class for SpringSecurity that requires it to inherit the WebSecurityConfigurerAdapter.

Configuration classes are placed under the config package

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

}
4.3.3.2, Landing Interface

Next we need to customize the login interface and let SpringSecurity release it so that users can access it without having to sign in.

In the interface we authenticate users through the AuthenticationManager authenticate method, so we need to configure AuthenticationManager to be injected into the container in SecurityConfig.

If the authentication is successful, a jwt is generated and returned in the response. And in order for users to be able to identify who they are through jwt the next time they request, we need to store the user information in redis, using the user id as the key.

LoginController

package com.qx.controller;

import com.qx.entity.User;
import com.qx.service.LoginService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author : k
 * @Date : 2022/3/23
 * @Desc :
 */
@RestController
public class LoginController {
    @Autowired
    private LoginService loginService;

    @PostMapping("/user/login")
    public ResponseResult login(@RequestBody User user){
       return loginService.login(user);
    }

}

Develop login interface

User authentication through Authentication Manager's authenticate method requires Authentication Manager to be configured in SecurityConfig to be injected into the container

SecurityConfig

/**
 * @Author Three stop B: https://space.bilibili.com/663528522
 */
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {


    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //Close csrf
                .csrf().disable()
                //Get SecurityContext without Session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // Allow anonymous access for login interfaces
                .antMatchers("/user/login").anonymous()
                // All requests except above require authentication
                .anyRequest().authenticated();
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

Login interface: LoginService

package com.qx.service;

import com.qx.controller.ResponseResult;
import com.qx.entity.User;

/**
 * @author : k
 * @Date : 2022/3/23
 * @Desc :
 */
public interface LoginService {
    ResponseResult login(User user);

    ResponseResult logout();

}

Login interface implementation class:

User authentication through Authentication Manager's authenticate method requires Authentication Manager to be configured in SecurityConfig to be injected into the container

Authentication implements LoginServiceImpl

package com.qx.service.impl;

import com.qx.entity.LoginUser;
import com.qx.controller.ResponseResult;
import com.qx.entity.User;
import com.qx.service.LoginService;
import com.qx.utils.JwtUtil;
import com.qx.utils.RedisCache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

/**
 * @author : k
 * @Date : 2022/3/23
 * @Desc :
 */
@Service
public class LoginServiceImpl implements LoginService {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private RedisCache redisCache;


    @Override
    public ResponseResult login(User user) {

       //Obtain username and password from UsernamePasswordAuthenticationToken 
        UsernamePasswordAuthenticationToken authenticationToken=new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword());
        
        //AuthenticationManager delegation mechanism authenticationToken user authentication
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);

        //If the authentication fails, give the corresponding prompt
        if (Objects.isNull(authenticate)){
            throw new RuntimeException("Logon Failure");
        }

        //If the certification passes, use user to generate JWT JWT and save it in ResponseResult to return
        
        //Get this current logged-in user information if the authentication passes
        LoginUser loginUser = (LoginUser) authenticate.getPrincipal();

         //Get the userid of the current user
        String userid = loginUser.getUser().getId().toString();

        String jwt = JwtUtil.createJWT(userid);
        Map<String, String> map = new HashMap<>();
        map.put("token",jwt);

        //Save complete user information in redis userid as key user information as value
        redisCache.setCacheObject("login:"+userid,loginUser);

        return new ResponseResult(200,"Login Successful",map);
    }

}
4.3.3.3, Authentication filter

We need to customize a filter that gets the token in the request header and parses the token to get the userid out of it.

Use userid to get the corresponding LoginUser object in redis. Then encapsulate the Authentication object and save it in SecurityContextHolder

JwtAuthenticationTokenFilter

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Autowired
    private RedisCache redisCache;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        //Get token
        String token = request.getHeader("token");
        if (!StringUtils.hasText(token)) {
            //Release
            filterChain.doFilter(request, response);
            return;
        }
        //Resolve token
        String userid;
        try {
            Claims claims = JwtUtil.parseJWT(token);
            userid = claims.getSubject();
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("token illegal");
        }
        //Get user information from redis
        String redisKey = "login:" + userid;
        LoginUser loginUser = redisCache.getCacheObject(redisKey);
        if(Objects.isNull(loginUser)){
            throw new RuntimeException("User not logged in");
        }
        
        //Encapsulate Authentication object into SecurityContextHolder
        //TODO get permission information encapsulated in Authentication
        
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(loginUser,null,null);
        
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        //Release
        filterChain.doFilter(request, response);
    }
}

SecurityConfig

//Add a token check filter to the filter chain
    http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);


@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {


    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }


    @Autowired
    JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //Close csrf
                .csrf().disable()
                //Get SecurityContext without Session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // Allow anonymous access for login interfaces
                .antMatchers("/user/login").anonymous()
                // All requests except above require authentication
                .anyRequest().authenticated();

        //Add a token check filter to the filter chain
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}
4.3.3.4, Exit landing

We just need to define a login interface, get the authentication information in the SecurityContextHolder, and delete the corresponding data in redis.

service layer

LoginService

package com.qx.service;

import com.qx.controller.ResponseResult;
import com.qx.entity.User;

/**
 * @author : k
 * @Date : 2022/3/23
 * @Desc :
 */
public interface LoginService {
    ResponseResult login(User user);

    ResponseResult logout();

}

Implementation Class

LoginServiceImpl

 @Override
    public ResponseResult logout() {
        //userid from SecurityContextHolder
        UsernamePasswordAuthenticationToken authentication =
                (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();

        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        Long userid = loginUser.getUser().getId();

        //Delete redis corresponding values based on userid
        redisCache.deleteObject("login:"+userid);
        return new ResponseResult(200,"Logoff successful");
    }


package com.qx.service.impl;

import com.qx.entity.LoginUser;
import com.qx.controller.ResponseResult;
import com.qx.entity.User;
import com.qx.service.LoginService;
import com.qx.utils.JwtUtil;
import com.qx.utils.RedisCache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

/**
 * @author : k
 * @Date : 2022/3/23
 * @Desc :
 */
@Service
public class LoginServiceImpl implements LoginService {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private RedisCache redisCache;


    //Authentication
    @Override
    public ResponseResult login(User user) {

        //Obtain username and password from UsernamePasswordAuthenticationToken
        UsernamePasswordAuthenticationToken authenticationToken=new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword());
        //AuthenticationManager delegation mechanism authenticationToken user authentication
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);

        //If the authentication fails, give the corresponding prompt
        if (Objects.isNull(authenticate)){
            throw new RuntimeException("Logon Failure");
        }

        //If the certification passes, use user to generate JWT JWT and save it in ResponseResult to return

        //Get this current logged-in user information if the authentication passes
        LoginUser loginUser = (LoginUser) authenticate.getPrincipal();

        //Get the userid of the current user
        String userid = loginUser.getUser().getId().toString();


        String jwt = JwtUtil.createJWT(userid);
        Map<String, String> map = new HashMap<>();
        map.put("token",jwt);

        //Save complete user information in redis userid as key user information as value
        redisCache.setCacheObject("login:"+userid,loginUser);

        return new ResponseResult(200,"Login Successful",map);
    }



    @Override
    public ResponseResult logout() {
        //userid from SecurityContextHolder
        UsernamePasswordAuthenticationToken authentication =
                (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();

        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        Long userid = loginUser.getUser().getId();

        //Delete redis corresponding values based on userid
        redisCache.deleteObject("login:"+userid);
        return new ResponseResult(200,"Logoff successful");
    }
}

controller Layer

LoginController

@RestController
public class LoginController {

    @Autowired
    private LoginService loginService;

    @PostMapping("/user/login")
    public ResponseResult login(@RequestBody User user){
       return loginService.login(user);
    }

    @PostMapping("/user/logout")
    public ResponseResult logout(){
       return loginService.logout();

    }

}

5. Authorization

5.1. Role of privileges

For example, a management system of a school library, if an ordinary student logs in and can see the functions related to borrowing and returning books, it is impossible for him to see and use such functions as adding book information and deleting book information. But if a librarian's account is logged in, you should be able to see and use functions such as adding book information, deleting book information, and so on.

To sum up, different users can use different functions. This is what the permission system does.

We can't just rely on the front end to determine which menus and buttons to display. Because if this is the only way, if someone knows the interface address of the corresponding function, they can send a request directly without going through the front end to implement the related function operation.

Therefore, we also need to make user rights judgment in the background, to determine whether the current user has the appropriate rights, we must have the required rights to carry out the appropriate operation.

5.2. Basic Authorization Process

In SpringSecurity, the default FilterSecurityInterceptor is used for privilege checking. In FilterSecurityInterceptor, the Authentication is obtained from the SecurityContextHolder and the permission information is obtained from it. Does the current user have the permissions required to access the current resource?

So all we need to do in our project is to save the privilege information of the currently logged-in user in Authentication as well. Then set the permissions we need for our resources.

5.3. Authorization implementation

5.3.1. Restricting permissions required to access resources

SpringSecurity provides us with a comment-based permission control scheme, which is the main approach used in our project. We can use annotations to specify the permissions required to access the corresponding resources.

But to use it we need to turn on the configuration first.

SecurityConfig

Add the following words to the class to turn on annotation

@EnableGlobalMethodSecurity(prePostEnabled =true) //Turn on Authorization Annotation

You can use the corresponding comment. @ PreAuthorize

/**
 * @author : k
 * @Date : 2022/3/23
 * @Desc :
 */
@RestController
public class HelloController {
    @RequestMapping("/hello")
    @PreAuthorize("hasAuthority('test')")
    public String hello(){
        return "hello";
    }
}

5.3.2, Encapsulate permission information

As we said earlier when writing UserDetailsServiceImpl, after querying out the user, we also need to get the corresponding permission information, which is encapsulated and returned in UserDetails. //TODO labeling performed

Let's test by writing the permission information to UserDetails directly.

We have previously defined the implementation class LoginUser for UserDetails, which needs to be modified to encapsulate permission information.

LoginUser

package com.qx.entity;

import com.alibaba.fastjson.annotation.JSONField;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;

import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;

@Data
@NoArgsConstructor
public class LoginUser implements UserDetails {

    private User user;

    //Stores permission information for the currently logged in user, a user can have multiple permissions
    private List<String> permissions;

    public LoginUser(User user, List<String> permissions) {
        this.user = user;
        this.permissions = permissions;
    }

    //Permission Set
    @JSONField(serialize = false)
    private  List<SimpleGrantedAuthority>  authorities;

    //Get permission information
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {

        if (authorities!=null){
            return authorities;
        }


        //Encapsulate permission information of String type in permissions as SimpleGrantedAuthority
        //First way
//         List<GrantedAuthority> newList = new ArrayList<>();
//        for (String permission : permissions) {
//            SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permission);
//            newList.add(authority);
//        }

        //Mode 2
      authorities = permissions.stream().
                map(SimpleGrantedAuthority::new).
                collect(Collectors.toList());

        return authorities;
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getUserName();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

[External chain picture transfer failed, source station may have anti-theft chain mechanism, it is recommended to save the picture and upload it directly (img-JR7XbX2e-1648310991192) (C:Users?66AppDataRoaming Typora-user-imagesimage-20220326193136483.png)]

After the LoginUser is modified, we can encapsulate the permission information in the UserDetailsServiceImpl into LoginUser. We test the write-to-death permissions, and then we query the permission information from the database.

package com.qx.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.qx.entity.LoginUser;
import com.qx.entity.User;
import com.qx.mapper.MenuMapper;
import com.qx.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Objects;

/**
 * @author : k
 * @Date : 2022/3/23
 * @Desc :
 */
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;


    //Implement the UserDetailsService interface, override the UserDetails method, and query custom user information from data
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        //(Authentication, which verifies the existence of the user) Query user information
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(User::getUserName,username);
        User user = userMapper.selectOne(queryWrapper);
        //If no user is queried
        if (Objects.isNull(user)){
            throw new RuntimeException("User name or password error");
        }


        //TODO (Authorization, i.e. Query what permissions a user has) Query the corresponding user information
        //Define a permission set
        List<String> list = new ArrayList<String>(Arrays.asList("test","admin"));
      
        //Encapsulate data as UserDetails to return
        return new LoginUser(user,list);
    }
}

5.3.3 Query permission information from a database

5.3.3.1 RBAC Permission Model

RBAC permission model (Role-Based Access Control) is role-based permission control. This is currently the most commonly used and relatively easy-to-use, common permission model by developers.

5.3.3.2 Preparations

sql

sys_menu: permission table

sys_role:role table

sys_role_menu: role permissions table

sys_user_role: user role table

sys_user:user table

So that we can use sys_later User connects to sys_user_role table, sys_user_role connects to sys_role table gets the user's role, sys_role table connected to sys_role_menu table, what permissions do users have ultimately

sys_user:

sys_user_role:

sys_role:

sys_role_menu:

sys_menu:

CREATE DATABASE /*!32312 IF NOT EXISTS*/`sg_security` /*!40100 DEFAULT CHARACTER SET utf8mb4 */;

USE `sg_security`;

/*Table structure for table `sys_menu` */

DROP TABLE IF EXISTS `sys_menu`;

CREATE TABLE `sys_menu` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `menu_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT 'menu name',
  `path` varchar(200) DEFAULT NULL COMMENT 'Routing Address',
  `component` varchar(255) DEFAULT NULL COMMENT 'Component Path',
  `visible` char(1) DEFAULT '0' COMMENT 'Menu status (0 shows 1 hides)',
  `status` char(1) DEFAULT '0' COMMENT 'Menu status (0 normal 1 disabled)',
  `perms` varchar(100) DEFAULT NULL COMMENT 'Permission Identification',
  `icon` varchar(100) DEFAULT '#'COMMENT'menu icon',
  `create_by` bigint(20) DEFAULT NULL,
  `create_time` datetime DEFAULT NULL,
  `update_by` bigint(20) DEFAULT NULL,
  `update_time` datetime DEFAULT NULL,
  `del_flag` int(11) DEFAULT '0' COMMENT 'Delete (0 not deleted 1 deleted)',
  `remark` varchar(500) DEFAULT NULL COMMENT 'Remarks',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='Menu Table';

/*Table structure for table `sys_role` */

DROP TABLE IF EXISTS `sys_role`;

CREATE TABLE `sys_role` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `name` varchar(128) DEFAULT NULL,
  `role_key` varchar(100) DEFAULT NULL COMMENT 'Role Permission String',
  `status` char(1) DEFAULT '0' COMMENT 'Role Status (0 Normal 1 Deactivated)',
  `del_flag` int(1) DEFAULT '0' COMMENT 'del_flag',
  `create_by` bigint(200) DEFAULT NULL,
  `create_time` datetime DEFAULT NULL,
  `update_by` bigint(200) DEFAULT NULL,
  `update_time` datetime DEFAULT NULL,
  `remark` varchar(500) DEFAULT NULL COMMENT 'Remarks',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='Role Table';

/*Table structure for table `sys_role_menu` */

DROP TABLE IF EXISTS `sys_role_menu`;

CREATE TABLE `sys_role_menu` (
  `role_id` bigint(200) NOT NULL AUTO_INCREMENT COMMENT 'role ID',
  `menu_id` bigint(200) NOT NULL DEFAULT '0' COMMENT 'menu id',
  PRIMARY KEY (`role_id`,`menu_id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;

/*Table structure for table `sys_user` */

DROP TABLE IF EXISTS `sys_user`;

CREATE TABLE `sys_user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'Primary key',
  `user_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT 'User name',
  `nick_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT 'Nickname?',
  `password` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT 'Password',
  `status` char(1) DEFAULT '0' COMMENT 'Account status (0 normal 1 deactivated)',
  `email` varchar(64) DEFAULT NULL COMMENT 'mailbox',
  `phonenumber` varchar(32) DEFAULT NULL COMMENT 'Cell-phone number',
  `sex` char(1) DEFAULT NULL COMMENT 'User gender (0 men, 1 woman, 2 unknown)',
  `avatar` varchar(128) DEFAULT NULL COMMENT 'Head portrait',
  `user_type` char(1) NOT NULL DEFAULT '1' COMMENT 'User type (0 administrator, 1 normal user)',
  `create_by` bigint(20) DEFAULT NULL COMMENT 'Creator's user id',
  `create_time` datetime DEFAULT NULL COMMENT 'Creation Time',
  `update_by` bigint(20) DEFAULT NULL COMMENT 'Update Person',
  `update_time` datetime DEFAULT NULL COMMENT 'Update Time',
  `del_flag` int(11) DEFAULT '0' COMMENT 'Delete flag (0 for not deleted, 1 for deleted)',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='User table';

/*Table structure for table `sys_user_role` */

DROP TABLE IF EXISTS `sys_user_role`;

CREATE TABLE `sys_user_role` (
  `user_id` bigint(200) NOT NULL AUTO_INCREMENT COMMENT 'user id',
  `role_id` bigint(200) NOT NULL DEFAULT '0' COMMENT 'role id',
  PRIMARY KEY (`user_id`,`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

Query what permissions the user has for sql statements:

# The role s and menu s corresponding to perms queried by userid must be in normal state

select distinct m.perms from sys_user_role ur
left join sys_role r on ur.role_id=r.id
left join sys_role_menu rm on ur.role_id=rm.role_id
left join sys_menu m on m.id=rm.menu_id
where user_id=2
and r.status=0
and m.status=0

Entity class:

Menu

package com.qx.entity;

import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;
import java.util.Date;

/**
 * Menu Table Entity Class
 *
 */
@TableName(value="sys_menu")
@Data
@AllArgsConstructor
@NoArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Menu implements Serializable {
    private static final long serialVersionUID = -54979041104113736L;
    
    @TableId
    private Long id;
    /**
    * menu name
    */
    private String menuName;
    /**
    * Routing Address
    */
    private String path;
    /**
    * Component Path
    */
    private String component;
    /**
    * Menu status (0 shows 1 hides)
    */
    private String visible;
    /**
    * Menu status (0 normal 1 disabled)
    */
    private String status;
    /**
    * Permission Identification
    */
    private String perms;
    /**
    * Menu Icon
    */
    private String icon;
    
    private Long createBy;
    
    private Date createTime;
    
    private Long updateBy;
    
    private Date updateTime;
    /**
    * Delete (0 not deleted 1 deleted)
    */
    private Integer delFlag;
    /**
    * Remarks
    */
    private String remark;
}
5.3.3.3, Code implementation

We only need to query the corresponding permission information based on the user id.

So we can define a mapper first, which provides a way to query permission information based on the userid.

MenuMapper

package com.qx.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.qx.entity.Menu;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;

import java.util.List;

/**
 * @author : k
 * @Date : 2022/3/24
 * @Desc :
 */
@Mapper
@Repository
public interface MenuMapper extends BaseMapper<Menu> {


    List<String> selectPermsByUserId(Long userid);


}

Create the corresponding Mapper.xml file, defining the corresponding sql statement MenuMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.qx.mapper.MenuMapper">


    <select id="selectPermsByUserId" resultType="java.lang.String">

        select distinct m.perms
        from sys_user_role ur
                 left join sys_role r on ur.role_id = r.id
                 left join sys_role_menu rm on ur.role_id = rm.role_id
                 left join sys_menu m on m.id = rm.menu_id
        where user_id = #{userid}
          and r.status = 0
          and m.status = 0

    </select>
</mapper>

Configure the location of the mapperXML file in application.yml

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/qx_security?characterEncoding=utf-8&serverTimezone=UTC
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver

mybatis-plus:
  mapper-locations: classpath*:/mapper/**/*.xml

server:
  port: 8888

Then we can encapsulate the method query permission information that calls this mapper in the UserDetailsServiceImpl into the LoginUser object.

package com.qx.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.qx.entity.LoginUser;
import com.qx.entity.User;
import com.qx.mapper.MenuMapper;
import com.qx.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Objects;

/**
 * @author : k
 * @Date : 2022/3/23
 * @Desc :
 */
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;


    @Autowired
    private MenuMapper menuMapper;

    //Implement the UserDetailsService interface, override the UserDetails method, and query custom user information from data
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        //(Authentication, which verifies the existence of the user) Query user information
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(User::getUserName,username);
        User user = userMapper.selectOne(queryWrapper);
        //If no user is queried
        if (Objects.isNull(user)){
            throw new RuntimeException("User name or password error");
        }


        //TODO (Authorization, i.e. Query what permissions a user has) Query the corresponding user information
        //Define a permission set
//        List<String> list = new ArrayList<String>(Arrays.asList("test","admin"));
        List<String> list = menuMapper.selectPermsByUserId(user.getId());


        //Encapsulate data as UserDetails to return
        return new LoginUser(user,list);
    }
}

Test:

package com.qx.controller;

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.parameters.P;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author : k
 * @Date : 2022/3/23
 * @Desc :
 */
@RestController
public class HelloController {


    @RequestMapping("hello")
//    @PreAuthorize("hasAnyAuthority('admin','test','system:dept:list')")
//    @PreAuthorize ('hasRole ('system:dept:list')//Requires prefix ROLE_ To pass
//    @PreAuthorize ('hasAnyRole ('admin','system:dept:list')//Requires prefix ROLE_ To pass
//    @PreAuthorize("hasAuthority('system:dept:list111')")
    public String hello(){
        return "hello";
    }

}

6. Custom Failure Handling

We also want to return the same structured json as our interface if authentication fails or authorization fails so that the front end can handle the response uniformly. To do this, we need to know the exception handling mechanism of SpringSecurity.

In SpringSecurity, if an exception occurs during authentication or authorization, it will be caught by ExceptionTranslationFilter. ExceptionTranslationFilter is used to determine whether an exception occurs when authentication or authorization fails.

If an exception occurs during authentication, it is encapsulated as AuthenticationException and then invoked the method of the AuthenticationEntryPoint object to handle the exception.

If an exception occurs during authorization, it is encapsulated as AccessDeniedException and then invoked the method of the AccessDeniedHandler object for exception handling.

So if we need to customize exception handling, we just need to customize AuthenticationEntryPoint and AccessDeniedHandler and configure it to SpringSecurity.

6.1. Custom implementation class

AuthenticationEntryPointImpl

package com.qx.handler;

import com.alibaba.fastjson.JSON;
import com.qx.controller.ResponseResult;
import com.qx.utils.WebUtils;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author : k
 * @Date : 2022/3/24
 * @Desc : Authenticated exception handling class
 */
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        ResponseResult result = new ResponseResult(HttpStatus.UNAUTHORIZED.value(),"User Name Authentication Failed Please Log on Again");
        String json = JSON.toJSONString(result);
        //Processing Removal
        WebUtils.renderString(response,json);
    }
}

AccessDeniedHandlerImpl

package com.qx.handler;

import com.alibaba.fastjson.JSON;
import com.qx.controller.ResponseResult;
import com.qx.utils.WebUtils;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author : k
 * @Date : 2022/3/24
 * @Desc : Authorized exception handling
 */
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        ResponseResult result = new ResponseResult(HttpStatus.FORBIDDEN.value(),"You do not have sufficient privileges");
        String json = JSON.toJSONString(result);
        //Processing Removal
        WebUtils.renderString(response,json);
    }
}

6.2, Configured to SpringSecurity

@Autowired
private AuthenticationEntryPoint authenticationEntryPoint;

@Autowired
private AccessDeniedHandler accessDeniedHandler;

Then we can configure it using the HttpSecurity object method.

//Configure exception handlers
http.exceptionHandling()
        //Authentication Failure Processor
        .authenticationEntryPoint(authenticationEntryPoint)
        .accessDeniedHandler(accessDeniedHandler);

7. Cross-domain

For security reasons, browsers must follow the homology policy when making HTTP requests using the XMLHttpRequest object, otherwise cross-domain HTTP requests are prohibited by default. Homology policy requires the same source to communicate properly, that is, protocol, domain name, port number are identical.

Front-end and back-end projects are not always of the same origin, so cross-domain requests are bound to be a problem.

So we have to deal with this so that the front end can make cross-domain requests.

SpringBoot configuration to run cross-domain requests

CorsConfig

package com.qx.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class CorsConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
      // Set paths that allow cross-domain
        registry.addMapping("/**")
                // Set up domain names that allow cross-domain requests
                .allowedOriginPatterns("*")
                // Allow cookie s
                .allowCredentials(true)
                // Set allowed request mode
                .allowedMethods("GET", "POST", "DELETE", "PUT")
                // Setting the allowed header property
                .allowedHeaders("*")
                // Cross-domain Allow Time
                .maxAge(3600);
    }
}

Turn on cross-domain access to open SpringSecurity in SecurityConfig

Since our resources are protected by SpringSecurity, we also need SpringSecurity to run cross-domain access if we want cross-domain access.

package com.qx.config;

import com.qx.filter.JwtAuthenticationTokenFilter;
import com.qx.handler.AccessDeniedHandlerImpl;
import com.qx.handler.AuthenticationEntryPointImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import sun.security.util.Password;

/**
 * @author : k
 * @Date : 2022/3/23
 * @Desc :
 */
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled =true) //Turn on Authorization Annotation
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    @Autowired
    private AuthenticationEntryPoint authenticationEntryPoint;


    @Autowired
    private AccessDeniedHandler accessDeniedHandler;

    //Release
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //Close csrf
                .csrf().disable()
                //Get SecurityContext without Session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // Allow anonymous access for login interfaces
                .antMatchers("/user/login").anonymous()
                .antMatchers("/testCors").hasAuthority("system:dept:list211")
                // All requests except above require authentication
                .anyRequest().authenticated();

        //Place the jwtAuthentication TokenFilter filter before login authentication
        http.addFilterBefore(jwtAuthenticationTokenFilter,UsernamePasswordAuthenticationFilter.class);


        //Configure exception handlers
        http.exceptionHandling()
                //Authentication Failure Processor
                .authenticationEntryPoint(authenticationEntryPoint)
                .accessDeniedHandler(accessDeniedHandler);


        //Allow cross-domain
        http.cors();
    }

    @Bean  //This allows you to get AuthenticationManager from the container
    @Override
    protected AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }
}

8. Custom permission checking method

We can also define our own permission checking methods and use our methods in the @PreAuthorize comment.

package com.qx.expression;

import com.qx.entity.LoginUser;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;

import java.util.List;

/**
 * @author : k
 * @Date : 2022/3/24
 * @Desc :
 */
@Component("ex")
public class QXExpressionRoot {


    //String authority Here is the permission given to it by the backend
    //Obtaining the rights of the logged-in user from the database and comparing authority
    public boolean hasAuthority(String authority){
        //Get current user permissions
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        List<String> permissions = loginUser.getPermissions();
        //Determine if authority exists in the set of user rights
        return permissions.contains(authority);
    }

}

Using @ex in a SPEL L expression is equivalent to getting an object whose name is ex for a bean in a container. Then call the hasAuthority method on this object

    @RequestMapping("hello")
    //Customized permission functions
    @PreAuthorize("@ex.hasAuthority('system:dept:list')")
    public String hello(){
        return "hello";
    }

Configuration-based permission control

package com.qx.config;

import com.qx.filter.JwtAuthenticationTokenFilter;
import com.qx.handler.AccessDeniedHandlerImpl;
import com.qx.handler.AuthenticationEntryPointImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import sun.security.util.Password;

/**
 * @author : k
 * @Date : 2022/3/23
 * @Desc :
 */
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled =true) //Turn on Authorization Annotation
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    @Autowired
    private AuthenticationEntryPoint authenticationEntryPoint;


    @Autowired
    private AccessDeniedHandler accessDeniedHandler;

    //Release
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //Close csrf
                .csrf().disable()
                //Get SecurityContext without Session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // Allow anonymous access for login interfaces
                .antMatchers("/user/login").anonymous()
                .antMatchers("/testCors").hasAuthority("system:dept:list211")
                // All requests except above require authentication
                .anyRequest().authenticated();

        //Place the jwtAuthentication TokenFilter filter before login authentication
        http.addFilterBefore(jwtAuthenticationTokenFilter,UsernamePasswordAuthenticationFilter.class);


        //Configure exception handlers
        http.exceptionHandling()
                //Authentication Failure Processor
                .authenticationEntryPoint(authenticationEntryPoint)
                .accessDeniedHandler(accessDeniedHandler);


        //Allow cross-domain
        http.cors();
    }

    @Bean  //This allows you to get AuthenticationManager from the container
    @Override
    protected AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }
}

9,CSRF

CSRF refers to Cross-site request forgery, which is one of the common attacks on the web.

https://blog.csdn.net/freeking101/article/details/86537087

SpringSecurity prevents CSRF attacks by using csrf_token. The backend generates a csrf_token, this csrf_needs to be brought with the front end when making a request Token, the backend will have filters to verify, and access will not be allowed without carrying or forging them.

We can see that CSRF attacks rely on authentication information carried in cookies. But in front-end and back-end separated projects, our authentication information is actually token, which is not stored in cookies, and requires front-end code to set token in the request header, so there is no need to worry about CSRF attacks.

10. Authentication Processor

10.1, Authentication Success Processor

In fact, when UsernamePasswordAuthenticationFilter authenticates a login, if the login succeeds, the AuthenticationSuccessHandler method is invoked to handle the process after the authentication succeeds. AuthenticationSuccessHandler is the Login Success Processor.

We can also customize the successful processor to handle the success accordingly.

We can test it in our entry case:

QXSuccessHandler

package com.qx.handler;

import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author : k
 * @Date : 2022/3/24
 * @Desc :
 */
@Component
public class QXSuccessHandler implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        System.out.println("Authentication Successful");
    }
}

SecurityConfig

package com.qx.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;

import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;

/**
 * @author : k
 * @Date : 2022/3/24
 * @Desc :
 */
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter  {

    @Autowired
    private AuthenticationSuccessHandler authencationSuccessHandler;

    @Autowired
    private AuthenticationFailureHandler authencationFailureHandler;

    @Autowired
    private LogoutSuccessHandler logoutSuccessHandler;
    
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin().
                //Configure Authentication Success Processor
                successHandler(authencationSuccessHandler)
                //Configure Authentication Failure Processor
                .failureHandler(authencationFailureHandler);

        //Configure Logoff Success Processor
        http.logout().logoutSuccessHandler(logoutSuccessHandler);


        //Authentication rules need to be added manually because they are overridden
        http.authorizeRequests().anyRequest().authenticated();
    }
}

10.2, Authentication Failure Processor

QXFailureHandler

package com.qx.handler;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author : k
 * @Date : 2022/3/24
 * @Desc :
 */
@Component
public class QXFailureHandler implements AuthenticationFailureHandler{


    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        System.out.println("Authentication Failure");
    }
}

SecurityConfig

package com.qx.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;

import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;

/**
 * @author : k
 * @Date : 2022/3/24
 * @Desc :
 */
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter  {

    @Autowired
    private AuthenticationSuccessHandler authencationSuccessHandler;

    @Autowired
    private AuthenticationFailureHandler authencationFailureHandler;

    @Autowired
    private LogoutSuccessHandler logoutSuccessHandler;
    
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin().
                //Configure Authentication Success Processor
                successHandler(authencationSuccessHandler)
                //Configure Authentication Failure Processor
                .failureHandler(authencationFailureHandler);

        //Configure Logoff Success Processor
        http.logout().logoutSuccessHandler(logoutSuccessHandler);


        //Authentication rules need to be added manually because they are overridden
        http.authorizeRequests().anyRequest().authenticated();
    }
}

10.3, Logoff Success Processor

QXLogoutSuccessHandler

package com.qx.handler;

import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author : k
 * @Date : 2022/3/24
 * @Desc :
 */
@Component
public class QXLogoutSuccessHandler implements LogoutSuccessHandler {
    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        System.out.println("Logoff successful");
    }
}

SecurityConfig

package com.qx.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;

import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;

/**
 * @author : k
 * @Date : 2022/3/24
 * @Desc :
 */
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter  {

    @Autowired
    private AuthenticationSuccessHandler authencationSuccessHandler;

    @Autowired
    private AuthenticationFailureHandler authencationFailureHandler;

    @Autowired
    private LogoutSuccessHandler logoutSuccessHandler;
    
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin().
                //Configure Authentication Success Processor
                successHandler(authencationSuccessHandler)
                //Configure Authentication Failure Processor
                .failureHandler(authencationFailureHandler);

        //Configure Logoff Success Processor
        http.logout().logoutSuccessHandler(logoutSuccessHandler);


        //Authentication rules need to be added manually because they are overridden
        http.authorizeRequests().anyRequest().authenticated();
    }
}

Eggs:

1. SecurityContextHolder: Used to store security context SecurityContext information, which further holds all the information that Authentication represents for the current user: who is the user, whether it has been authenticated, what roles it holds... The information is stored in Anthentication.

By default, the SecurityContextHolder uses the ThreadLocal policy to store authentication information, which means that it is a thread-binding policy where Spring Security automatically binds authentication information to the current thread when the user logs on and automatically knows the authentication information of the current thread when the user exits.

2,Authentication

package org.springframework.security.core;

import java.io.Serializable;
import java.security.Principal;
import java.util.Collection;

import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.context.SecurityContextHolder;


public interface Authentication extends Principal, Serializable {

   //In the permission collection is SimpleGrantedAuthority, a subclass of GrantedAuthority
   Collection<? extends  GrantedAuthority> getAuthorities();
    
   Object getCredentials(); //Get the credential information, that is, the password

   Object getDetails(); //Details such as ip adress are saved in Details

   Object getPrincipal();  //Get details of the subject Principal user An Object object

   boolean isAuthenticated();//Is Certified

   void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException; //set method

}

3,GrantedAuthority

package org.springframework.security.core;

import java.io.Serializable;

import org.springframework.security.access.AccessDecisionManager;

public interface GrantedAuthority extends Serializable {

   String getAuthority();  //Certified

}

Simple Authorization, Permissions

package org.springframework.security.core.authority;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityCoreVersion;
import org.springframework.util.Assert;

public final class SimpleGrantedAuthority implements GrantedAuthority {


   private final String role;  

   public SimpleGrantedAuthority(String role) {
      Assert.hasText(role, "A granted authority textual representation is required");
      this.role = role;
   }

   @Override
   public String getAuthority() {
      return this.role;
   }

That is, the Authority permission object is stored inside GrantedAuthority

4,UserDetails

package org.springframework.security.core.userdetails;

import java.io.Serializable;
import java.util.Collection;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;

public interface UserDetails extends Serializable {

   Collection<? extends GrantedAuthority> getAuthorities();//Permission Identity Collection

   String getPassword(); //Password

  
   String getUsername();  //User name


   boolean isAccountNonExpired();  //Is it not expired


   boolean isAccountNonLocked(); //Is it unlocked


   boolean isCredentialsNonExpired();  //Does the voucher not expire

 
   boolean isEnabled();  //Is Available

}

5,UserDetailsService

public interface UserDetailsService {

 	//Query UserDetails through username 
   UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;

}

6. AuthenticationManager Certification Manager

public interface AuthenticationManager {
	
    //Authentication authenticate s the incoming authentication 
   Authentication authenticate(Authentication authentication) throws AuthenticationException;

}

7. ProviderManager is the implementation class of AuthenticationManager

package org.springframework.security.authentication;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceAware;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.core.log.LogMessage;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.CredentialsContainer;
import org.springframework.security.core.SpringSecurityMessageSource;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;


public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {

   private static final Log logger = LogFactory.getLog(ProviderManager.class);

   private AuthenticationEventPublisher eventPublisher = new NullEventPublisher();

   private List<AuthenticationProvider> providers = Collections.emptyList();

   protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();

   private AuthenticationManager parent;

   private boolean eraseCredentialsAfterAuthentication = true;

   
   public ProviderManager(AuthenticationProvider... providers) {
      this(Arrays.asList(providers), null);
   }

  
   public ProviderManager(List<AuthenticationProvider> providers) {
      this(providers, null);
   }

  

   public ProviderManager(List<AuthenticationProvider> providers, AuthenticationManager parent) {
      Assert.notNull(providers, "providers list cannot be null");
      this.providers = providers;
      this.parent = parent;
      checkState();
   }

   @Override
   public void afterPropertiesSet() {
      checkState();
   }

   private void checkState() {
      Assert.isTrue(this.parent != null || !this.providers.isEmpty(),
            "A parent AuthenticationManager or a list of AuthenticationProviders is required");
      Assert.isTrue(!CollectionUtils.contains(this.providers.iterator(), null),
            "providers list cannot contain null values");
   }

 
    //Method Focus for Implementing AuthenticationManager Interface
   @Override
   public Authentication authenticate(Authentication authentication) throws AuthenticationException {
      Class<? extends Authentication> toTest = authentication.getClass();
      AuthenticationException lastException = null;
      AuthenticationException parentException = null;
      Authentication result = null;
      Authentication parentResult = null;
      int currentPosition = 0;
      int size = this.providers.size();
      for (AuthenticationProvider provider : getProviders()) {
         if (!provider.supports(toTest)) {
            continue;
         }
         if (logger.isTraceEnabled()) {
            logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)",
                  provider.getClass().getSimpleName(), ++currentPosition, size));
         }
         try {
            result = provider.authenticate(authentication);
            if (result != null) {
               copyDetails(authentication, result);
               break;
            }
         }
         catch (AccountStatusException | InternalAuthenticationServiceException ex) {
            prepareException(ex, authentication);
            // SEC-546: Avoid polling additional providers if auth failure is due to
            // invalid account status
            throw ex;
         }
         catch (AuthenticationException ex) {
            lastException = ex;
         }
      }
      if (result == null && this.parent != null) {
         // Allow the parent to try.
         try {
            parentResult = this.parent.authenticate(authentication);
            result = parentResult;
         }
         catch (ProviderNotFoundException ex) {
            // ignore as we will throw below if no other exception occurred prior to
            // calling parent and the parent
            // may throw ProviderNotFound even though a provider in the child already
            // handled the request
         }
         catch (AuthenticationException ex) {
            parentException = ex;
            lastException = ex;
         }
      }
      if (result != null) {
         if (this.eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) {
            // Authentication is complete. Remove credentials and other secret data
            // from authentication
            ((CredentialsContainer) result).eraseCredentials();
         }
         // If the parent AuthenticationManager was attempted and successful then it
         // will publish an AuthenticationSuccessEvent
         // This check prevents a duplicate AuthenticationSuccessEvent if the parent
         // AuthenticationManager already published it
         if (parentResult == null) {
            this.eventPublisher.publishAuthenticationSuccess(result);
         }

         return result;
      }

      // Parent was null, or didn't authenticate (or throw an exception).
      if (lastException == null) {
         lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound",
               new Object[] { toTest.getName() }, "No AuthenticationProvider found for {0}"));
      }
      // If the parent AuthenticationManager was attempted and failed then it will
      // publish an AbstractAuthenticationFailureEvent
      // This check prevents a duplicate AbstractAuthenticationFailureEvent if the
      // parent AuthenticationManager already published it
      if (parentException == null) {
         prepareException(lastException, authentication);
      }
      throw lastException;
   }

   @SuppressWarnings("deprecation")
   private void prepareException(AuthenticationException ex, Authentication auth) {
      this.eventPublisher.publishAuthenticationFailure(ex, auth);
   }

  
   private void copyDetails(Authentication source, Authentication dest) {
      if ((dest instanceof AbstractAuthenticationToken) && (dest.getDetails() == null)) {
         AbstractAuthenticationToken token = (AbstractAuthenticationToken) dest;
         token.setDetails(source.getDetails());
      }
   }

   public List<AuthenticationProvider> getProviders() {
      return this.providers;
   }

   @Override
   public void setMessageSource(MessageSource messageSource) {
      this.messages = new MessageSourceAccessor(messageSource);
   }

   public void setAuthenticationEventPublisher(AuthenticationEventPublisher eventPublisher) {
      Assert.notNull(eventPublisher, "AuthenticationEventPublisher cannot be null");
      this.eventPublisher = eventPublisher;
   }

  
   public void setEraseCredentialsAfterAuthentication(boolean eraseSecretData) {
      this.eraseCredentialsAfterAuthentication = eraseSecretData;
   }

   public boolean isEraseCredentialsAfterAuthentication() {
      return this.eraseCredentialsAfterAuthentication;
   }

   private static final class NullEventPublisher implements AuthenticationEventPublisher {

      @Override
      public void publishAuthenticationFailure(AuthenticationException exception, Authentication authentication) {
      }

      @Override
      public void publishAuthenticationSuccess(Authentication authentication) {
      }

   }

}

8,AuthenticationProvider

package org.springframework.security.authentication;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;

public interface AuthenticationProvider {

  //Authentication Certification
   Authentication authenticate(Authentication authentication) throws AuthenticationException;

	//Is incoming authentication supported
   boolean supports(Class<?> authentication);

}
        }
      }
      if (result != null) {
         if (this.eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) {
            // Authentication is complete. Remove credentials and other secret data
            // from authentication
            ((CredentialsContainer) result).eraseCredentials();
         }
         // If the parent AuthenticationManager was attempted and successful then it
         // will publish an AuthenticationSuccessEvent
         // This check prevents a duplicate AuthenticationSuccessEvent if the parent
         // AuthenticationManager already published it
         if (parentResult == null) {
            this.eventPublisher.publishAuthenticationSuccess(result);
         }

         return result;
      }

      // Parent was null, or didn't authenticate (or throw an exception).
      if (lastException == null) {
         lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound",
               new Object[] { toTest.getName() }, "No AuthenticationProvider found for {0}"));
      }
      // If the parent AuthenticationManager was attempted and failed then it will
      // publish an AbstractAuthenticationFailureEvent
      // This check prevents a duplicate AbstractAuthenticationFailureEvent if the
      // parent AuthenticationManager already published it
      if (parentException == null) {
         prepareException(lastException, authentication);
      }
      throw lastException;
   }

   @SuppressWarnings("deprecation")
   private void prepareException(AuthenticationException ex, Authentication auth) {
      this.eventPublisher.publishAuthenticationFailure(ex, auth);
   }

  
   private void copyDetails(Authentication source, Authentication dest) {
      if ((dest instanceof AbstractAuthenticationToken) && (dest.getDetails() == null)) {
         AbstractAuthenticationToken token = (AbstractAuthenticationToken) dest;
         token.setDetails(source.getDetails());
      }
   }

   public List<AuthenticationProvider> getProviders() {
      return this.providers;
   }

   @Override
   public void setMessageSource(MessageSource messageSource) {
      this.messages = new MessageSourceAccessor(messageSource);
   }

   public void setAuthenticationEventPublisher(AuthenticationEventPublisher eventPublisher) {
      Assert.notNull(eventPublisher, "AuthenticationEventPublisher cannot be null");
      this.eventPublisher = eventPublisher;
   }

  
   public void setEraseCredentialsAfterAuthentication(boolean eraseSecretData) {
      this.eraseCredentialsAfterAuthentication = eraseSecretData;
   }

   public boolean isEraseCredentialsAfterAuthentication() {
      return this.eraseCredentialsAfterAuthentication;
   }

   private static final class NullEventPublisher implements AuthenticationEventPublisher {

      @Override
      public void publishAuthenticationFailure(AuthenticationException exception, Authentication authentication) {
      }

      @Override
      public void publishAuthenticationSuccess(Authentication authentication) {
      }

   }

}

8,AuthenticationProvider

package org.springframework.security.authentication;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;

public interface AuthenticationProvider {

  //Authentication Certification
   Authentication authenticate(Authentication authentication) throws AuthenticationException;

	//Is incoming authentication supported
   boolean supports(Class<?> authentication);

}

First of all, I would like to introduce myself. Xiaobian graduated from Master Jiaotong University in 13 years, once stayed in a small company, went to factories such as Huawei OPPO, and entered Ali in 18 years until now. Knowing that most junior and intermediate Java engineers often need to grope for growth or sign up for classes to improve their skills, but the tuition fees of nearly 10,000 yuan for training institutions are very stressful. Their own self-study efficiency is low and long, and they are prone to encounter ceiling technology. So I have collected a complete set of learning materials for java development for you. The original intention is very simple. I want to help friends who want to learn by themselves but don't know where to start, and at the same time reduce the burden on you. Add the business card below to get the complete set of learning materials.

Tags: Android Interview Back-end Front-end

Posted by tibiz on Sun, 14 Aug 2022 22:59:38 +0530