[Wanzi long text] SpringBoot integrates SpringSecurity+JWT+Redis complete tutorial (Gitee source code provided)

Foreword: In the process of learning SpringSecurity recently, I referred to many online tutorials, and also referred to some current mainstream open source frameworks, so combined with my own ideas, I wrote a complete SpringBoot integration SpringSecurity+JWT+Redis project, from 0 to 1 After writing, I feel that I have gained a lot, so I wrote my complete notes as a blog to share with you. It is a relatively complete project, and it is only for your reference and study!

Table of contents

1. Introduction to Spring Security

Two, Spring Security authentication process

3. Problem record (important)

4. Explanation of the core code of the project

4.1. Import pom dependencies

4.2, yml configuration file

4.3, Entity class

4.3.1, LoginBody login entity class

4.3.2, Role role class

4.3.3, User user class

4.3.4, LoginUser login user information

4.4, TokenService service class

4.4.1. Generate Token Core Code

4.4.2. Key logic for generating tokens

4.4.3. Parse the token core code

4.4.4. Get the token carried in the request header

4.4.5. Get the token Key stored in Redis

4.4.6. Refresh Token Validity Period

4.4.7. Verification Token Validity Period

4.4.8. Obtain user identity information

4.4.9. Delete user identity information

4.5, AuthenticationEntryPointImpl configuration authentication failure processing class

4.6, JwtAuthenticationTokenFilter authentication filter

4.7, FastJson serialization

4.8, custom Redis serialization 

4.9, Redis tool class

4.10, SecurityConfig core configuration class

4.11, AuthenticationContextHolder thread local storage

4.12. UserServiceImpl query user interface

4.13, PasswordServiceImpl password verification service class 

4.14, UserDetailsServiceImpl authentication user service class

4.15, LoginController login interface

4.16, LoginServiceImpl login interface core logic

4.17, LogoutSuccessHandlerImpl logout core logic 

4.18. @PreAuthorize annotation

4.19, HelloController test interface

4.20 Summary

5. Run the project 

5.1. Successful login

5.2. Accessing Unauthorized Interfaces

5.3. Access to interfaces that require USER authority

5.4. Access to interfaces that require COMMON permissions

5.5. Log out

5.6. Access failure 

5.7. Login failed

6. Gitee source code address

7. Summary


1. Introduction to Spring Security

Spring Security is a security management framework in the Spring ecosystem, providing a complete solution for web application security.

It has the following characteristics:

1. Comprehensiveness: Spring Security provides all functions of security management such as authentication, authorization, and attack protection.

2. Extensibility: The functions of Spring Security can be easily extended by inheriting classes and implementing interfaces.

3. Seamless integration with Spring: It can be perfectly integrated with the Spring framework and manage SpringSecurity components through the SpringIoC container.

4. Prevent common attacks: It can prevent common web attacks such as script injection, session fixation, and SQL injection.

5. Simple configuration: The security functions brought by Spring Security can be quickly applied through configuration files.

The main functions of Spring Security include:

1. Authentication: verify the legitimacy of user identity information.

2. Authorization: Verify that the user has permission to perform the operation.

3. Protection against attacks: defense against attacks such as CSRF, Session fixation, and SQL injection.

4. Method security: implement security access control with system methods.

5. Security response header: Add browser security-related response headers to improve security.

In summary, Spring Security is an indispensable security protection framework for MVC applications, providing comprehensive security support for Java applications. It is tightly integrated with the Spring framework, easy to configure and easy to use.

Two, Spring Security authentication process

Several core classes and their functions in the Spring Security authentication process are as follows:

1. Authentication : authentication information interface, indicating the authentication information of the current user, usually implemented using UsernamePasswordAuthenticationToken .

2. AuthenticationManager : the authentication manager interface, the authenticate () method is used to execute the authentication process.

3. ProviderManager : A common implementation of the authentication manager interface, encapsulating multiple AuthenticationProviders .

4. AuthenticationProvider : a specific authentication processor, which completes a specific authentication mechanism .

5. UserDetailsService : load user information according to the username, and return the implementation of the UserDetails interface.

6. UserDetails : an interface that contains user information, and the frame represents user information .

7. UsernamePasswordAuthenticationFilter : A filter that handles form login authentication .

8. AbstractAuthenticationProcessingFilter : the base class of authentication processing filter.

9. SecurityContextHolder : Security context container, access to Authentication object. 

This is the complete SpringSecurity authentication process:

1. The user submits the user name and password to the system for authentication.

2. AuthenticationFilter will intercept the request, and extract the username and password from the request to construct a UsernamePasswordAuthenticationToken .

3. AuthenticationFilter transfers UsernamePasswordAuthenticationToken to AuthenticationManager .

4. AuthenticationManager will find a matching AuthenticationProvider for authentication.

5. AuthenticationProvider will first call the loadUserByUsername() method of UserDetailsService to load user information according to the username.

6. UserDetailsService queries the database according to the user name, and constructs a UserDetails object, including user information, permissions, etc.

7. AuthenticationProvider uses UserDetails and the password entered by the user to perform matching verification . If it matches, the verification is successful.

8. If the verification is successful, the AuthenticationProvider will construct an authenticated Authentication object.

9. AuthenticationProvider returns Authentication to AuthenticationManager . _

10. AuthenticationManager sets Authentication to SecurityContextHolder . _

11. Subsequent access control will use the Authentication information in SecurityContextHolder to verify user identity and authority.

12. The login is successful, and the user accesses the protected resources of the system.

The complete process is shown in the figure:

3. Problem record (important)

Read the following, please be sure to understand this error first:

autoType is not support.org.springframework.security.core.authority.SimpleGrantedAuthority error record (pro-test available)

4. Explanation of the core code of the project

Because the amount of code is relatively large, I will take out the key code of the whole project and explain it separately. I will not post the other minor ones. The main reason is to let everyone understand the execution process of Spring Security more easily. I will open source the code to Gitee and provide it at the end of the article.

4.1. Import pom dependencies

The complete dependencies are posted.

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

		<!-- lombok依赖包 -->
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<version>1.16.10</version>
			<scope>provided</scope>
		</dependency>


		<!-- spring security 安全认证 -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-security</artifactId>
		</dependency>

		<!-- 单元测试 -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>

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

		<!-- redis依赖 对象池 -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-redis</artifactId>
		</dependency>

		<!-- pool 对象池 -->
		<dependency>
			<groupId>org.apache.commons</groupId>
			<artifactId>commons-pool2</artifactId>
			<version>2.11.1</version>
		</dependency>

		<!-- 常用工具类 -->
		<dependency>
			<groupId>org.apache.commons</groupId>
			<artifactId>commons-lang3</artifactId>
			<version>3.9</version>
		</dependency>

		<!-- 阿里JSON解析器 -->
		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>fastjson</artifactId>
			<version>2.0.22</version>
		</dependency>

	</dependencies>

4.2, yml configuration file

It mainly configures a Redis connection and Token constant.

spring:
  redis:
    host: localhost
    port: 6379
    database: 0
    password:
    timeout: 10s
    lettuce:
      pool:
        min-idle: 0
        max-idle: 8
        max-active: 8
        max-wait: -1ms

token:
  header: Authorization
  secret: oqwe9sdladwosqwqs
  expireTime: 30

4.3, Entity class

A total of four entity classes are involved, and some key fields are mainly designed, which is not very complete.

4.3.1, LoginBody login entity class

This class is mainly used to receive the username and password passed by the front end, and then to verify the login information.

Full code:

package com.example.security.domain;

import lombok.Data;

@Data
public class LoginBody
{
    /**
     * 用户名
     */
    private String username;

    /**
     * 用户密码
     */
    private String password;

}

4.3.2, Role role class

To store the role information of each user, the serialization interface needs to be implemented.

Full code:

package com.example.security.domain;

import lombok.AllArgsConstructor;
import lombok.Data;

import java.io.Serializable;

@Data
@AllArgsConstructor
public class Role implements Serializable {

  /**
   * 角色主键
   */
  private Long id;

  /**
   * 角色名称
   */
  private String name;

}

4.3.3, User user class

It mainly stores user information and needs to implement the serialization interface.

Full code:

package com.example.security.domain;

import lombok.Data;

import java.io.Serializable;
import java.util.Set;

@Data
public class User implements Serializable {

    /**
     * 主键
     */
    private String id;

    /**
     * 用户名
     */
    private String username;

    /**
     * 密码
     */
    private String password;

    /**
     * 角色集合
     */
    private Set<Role> roles;


}

4.3.4, LoginUser login user information

It is necessary to implement the UserDetails interface that comes with Spring Security and implement all its methods. In Spring Security, we can use the GrantedAuthority interface to represent the permissions owned by a user.

method explain
isAccountNonExpired() Has the account expired?
isAccountNonLocked() Is the account locked?
isCredentialsNonExpired() Whether the credential (password) has expired
isEnabled() Is the account available

The purpose of these methods returning true is to simplify the logic. When the corresponding state judgment is not implemented, the default setting is true, which can avoid unnecessary authentication/authorization failures. 

Full code:

package com.example.security.domain;

import com.alibaba.fastjson2.annotation.JSONField;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.Set;

@Data
public class LoginUser implements UserDetails {

    public LoginUser(User user,Set<GrantedAuthority> authorities)
    {
        this.user = user;
        this.authorities = authorities;

    }

    /**
     * 用户信息
     */
    private User user;

    /**
     * 权限信息
     */
    private Set<GrantedAuthority> authorities;

    /**
     * token信息
     */
    private String token;

    /**
     * 登录时间
     */
    private Long loginTime;
    /**
     * 过期时间
     */
    private Long expireTime;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return this.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;
    }
}

4.4, TokenService service class

First inject some parameters.

key code:

@Value("${token.header}")
private String header;

@Value("${token.secret}")
private String secret;

@Value("${token.expireTime}")
private int expireTime;

protected static final long MILLIS_SECOND = 1000;

protected static final long MILLIS_MINUTE = 60 * MILLIS_SECOND;

private static final Long MILLIS_MINUTE_TEN = 20 * 60 * 1000L;

4.4.1. Generate Token Core Code

key code:

    private String generateToken(Map<String, Object> claims)
    {
        String token = Jwts.builder()
                .setClaims(claims)
                .signWith(SignatureAlgorithm.HS512, secret).compact();
        return token;
    }

4.4.2. Key logic for generating tokens

Ordinary LOGIN_TOKEN_KEY: login_tokens:

1. First generate a random UUID as the token value and set it to the LoginUser object.

2. Set the login time and expiration time of LoginUser (current time + expiration time).

3. Store the LoginUser object in Redis, the key is LOGIN_TOKEN_KEY+token, and the expiration time defaults to 30 minutes configured in yml.

4. Finally, call the generateToken method to generate a JWT token, and pass it into the claims hash Map collection.

key code:

    public String createToken(LoginUser loginUser)
    {
        String token = UUID.randomUUID().toString();
        loginUser.setToken(token);
        loginUser.setLoginTime(System.currentTimeMillis());
        loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
        String userKey = CacheConstants.LOGIN_TOKEN_KEY + token;
        redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
        Map<String, Object> claims = new HashMap<>();
        claims.put(Constants.LOGIN_USER_KEY, token);
        return generateToken(claims);
    }

4.4.3. Parse the token core code

key code:

 private Claims parseToken(String token)
    {
        return Jwts.parser()
                .setSigningKey(secret)
                .parseClaimsJws(token)
                .getBody();
    }

4.4.4. Get the token carried in the request header

Constant TOKEN_PREFIX: "Bearer"

1. Obtain the authorization information of the specified name (the header configured in the yml file) from the request header.

2. Determine whether the obtained token is not empty and starts with the specified prefix (Constants.TOKEN_PREFIX).

3. If yes, remove the prefix to get the final JWT token.

key code:

    private String getToken(HttpServletRequest request)
    {
        String token = request.getHeader(header);
        if (StringUtils.isNotEmpty(token) && token.startsWith(Constants.TOKEN_PREFIX))
        {
            token = token.replace(Constants.TOKEN_PREFIX, "");
        }
        return token;
    }

4.4.5. Get the token Key stored in Redis

key code:

    private String getTokenKey(String uuid)
    {
        return CacheConstants.LOGIN_TOKEN_KEY + uuid;
    }

4.4.6. Refresh Token Validity Period

1. The parameter loginUser is the current login user information.

2. Set the new login time of loginUser to the current time.

3. Recalculate the expiration time as the current time + expiration time (expireTime configured in the yml file).

4. According to the token of the login user as the key, store the updated loginUser in Redis and set the expiration time.

5. This is equivalent to refreshing the expiration time of the token.

key code:

    public void refreshToken(LoginUser loginUser)
    {
        loginUser.setLoginTime(System.currentTimeMillis());
        loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
        // 根据uuid将loginUser缓存
        String userKey = getTokenKey(loginUser.getToken());
        redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
    }

4.4.7. Verification Token Validity Period

The validity period of the verification token is less than 20 minutes, and the cache is automatically refreshed.

key code:

    public void verifyToken(LoginUser loginUser)
    {
        long expireTime = loginUser.getExpireTime();
        long currentTime = System.currentTimeMillis();
        if (expireTime - currentTime <= MILLIS_MINUTE_TEN)
        {
            refreshToken(loginUser);
        }
    }

4.4.8. Obtain user identity information

1. First obtain the JWT Token from the request.

2. If the Token is not empty, parse the Token to obtain claims.

3. Take out the corresponding uuid from claims.

4. Obtain the LoginUser object from Redis according to the uuid as the key.

5. If the acquisition is successful, the LoginUser object is returned.

6. If parsing the token or obtaining the user fails, return null.

key code:

    public LoginUser getLoginUser(HttpServletRequest request)
    {
        // 获取请求携带的令牌
        String token = getToken(request);
        if (StringUtils.isNotEmpty(token))
        {
            try
            {
                Claims claims = parseToken(token);
                // 解析对应的权限以及用户信息
                String uuid = (String) claims.get(Constants.LOGIN_USER_KEY);
                String userKey = getTokenKey(uuid);
                LoginUser user = redisCache.getCacheObject(userKey);
                return user;
            }
            catch (Exception e)
            {
            }
        }
        return null;
    }

4.4.9. Delete user identity information

Obtain the user's unique identifier in redis through token to clear the user's login data, and the next time you go through the filter, access will be restricted because there is no user information!

    /**
     * 删除用户身份信息
     */
    public void delLoginUser(String token)
    {
        if (StringUtils.isNotEmpty(token))
        {
            String userKey = getTokenKey(token);
            redisCache.deleteObject(userKey);
        }
    }

4.5, AuthenticationEntryPointImpl configuration authentication failure processing class

Constant UNAUTHORIZED: 401

AuthenticationEntryPoint is an interface used in Spring Security to handle authentication failures. It is used in the case of no login or login expiration, and the commence method will be triggered.

1. Inside the method, first set the response status code to 401 Unauthorized.

2. Then use StringUtils to generate an error message string, including the interface path of the requested access and the prompt of authentication failure. 3. Finally, use AjaxResult to encapsulate the status code and error information into a result, and write it into the response in JSON format through ServletUtils.

4. AjaxResult is a class that encapsulates AJAX request results, which can easily generate error or successful response results.

5. ServletUtils is a tool class that can easily render String data into HttpServletResponse.

Therefore, the function of this class is to return a result including error code and message to the front end in JSON format when the authentication fails. The front end can display corresponding prompts or do processing according to the result.

key code:

@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint
{
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException {
        int code = HttpStatus.UNAUTHORIZED;
        String msg = StringUtils.format("请求访问:{},认证失败,无法访问系统资源", request.getRequestURI());
        ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(code, msg)));
    }
}

4.6, JwtAuthenticationTokenFilter authentication filter

OncePerRequestFilter is a filter base class provided by SpringSecurity. It is mainly used to ensure that the filter is only executed once in a request. JwtAuthenticationTokenFilter needs to inherit this base class and rewrite the doFilterInternal method.

1. Extract the JWT token from the request header through tokenService, and parse to get the LoginUser object

2. Call the verifyToken method of tokenService to verify the validity of the JWT token

3. Use the LoginUser object to build a UsernamePasswordAuthenticationToken

4. Set the details of AuthenticationToken, such as the source of the request, etc.

5. Set the constructed UsernamePasswordAuthenticationToken object to the Context of SecurityContextHolder.

6. In this way, the Authentication object of the logged-in user is saved in the security context.

7. Finally, the filter chain continues to execute the doFilter method backwards.

In this way, the parsing and verification of the token is realized in the filter, and the Authentication object is set in the security context, and
subsequent filters can judge user authentication information based on it.

key code:

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter
{

    @Value("${token.header}")
    private String header;

    @Value("${token.secret}")
    private String tokenKey;

    @Resource
    private TokenService tokenService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException
    {

        LoginUser loginUser = tokenService.getLoginUser(request);
        if (loginUser != null)
        {
            tokenService.verifyToken(loginUser);
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
            authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        }
        chain.doFilter(request, response);
    }
}

 Note: If you want to get the currently logged in user information, you can get it through the following 2 lines of code.

Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
LoginUser loginUser = (LoginUser) authentication.getPrincipal();

4.7, FastJson serialization

package com.example.security.config;

import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONReader;
import com.alibaba.fastjson2.JSONWriter;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;

import java.nio.charset.Charset;

/**
 * Redis使用FastJson序列化
 */
public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
{
    public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");

    private Class<T> clazz;

    public FastJson2JsonRedisSerializer(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, JSONWriter.Feature.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, JSONReader.Feature.SupportAutoType);
    }
}

4.8, custom Redis serialization 

Note: The detailed explanation is in this article: SpringBoot integrates RedisTemplate to operate the Redis database in detail (Gitee source code is provided) 

package com.example.security.config;

import com.alibaba.fastjson.support.spring.GenericFastJsonRedisSerializer;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
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;

/**
 * redis配置
 */
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport
{
    @Bean
    @SuppressWarnings(value = { "unchecked", "rawtypes" })
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)
    {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);

        String[] acceptNames = {"org.springframework.security.core.authority.SimpleGrantedAuthority"};
        GenericFastJsonRedisSerializer serializer = new GenericFastJsonRedisSerializer(acceptNames);
        // 使用StringRedisSerializer来序列化和反序列化redis的key值
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(serializer);

        // Hash的key也采用StringRedisSerializer的序列化方式
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(serializer);

        template.afterPropertiesSet();
        return template;
    }

}

4.9, Redis tool class

package com.example.redis.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;
 
/**
 * spring redis 工具类
 **/
@SuppressWarnings(value = { "unchecked", "rawtypes" })
@Component
public class RedisCache
{
    @Autowired
    public RedisTemplate redisTemplate;
 
    /**
     * 缓存基本的对象,Integer、String、实体类等
     *
     * @param key 缓存的键值
     * @param value 缓存的值
     */
    public <T> void setCacheObject(final String key, final T value)
    {
        redisTemplate.opsForValue().set(key, value);
    }
 
    /**
     * 缓存基本的对象,Integer、String、实体类等
     *
     * @param key 缓存的键值
     * @param value 缓存的值
     * @param timeout 时间
     * @param timeUnit 时间颗粒度
     */
    public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit)
    {
        redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
    }
 
    /**
     * 设置有效时间
     *
     * @param key Redis键
     * @param timeout 超时时间
     * @return true=设置成功;false=设置失败
     */
    public boolean expire(final String key, final long timeout)
    {
        return expire(key, timeout, TimeUnit.SECONDS);
    }
 
    /**
     * 设置有效时间
     *
     * @param key Redis键
     * @param timeout 超时时间
     * @param unit 时间单位
     * @return true=设置成功;false=设置失败
     */
    public boolean expire(final String key, final long timeout, final TimeUnit unit)
    {
        return redisTemplate.expire(key, timeout, unit);
    }
 
    /**
     * 获取有效时间
     *
     * @param key Redis键
     * @return 有效时间
     */
    public long getExpire(final String key)
    {
        return redisTemplate.getExpire(key);
    }
 
    /**
     * 判断 key是否存在
     *
     * @param key 键
     * @return true 存在 false不存在
     */
    public Boolean hasKey(String key)
    {
        return redisTemplate.hasKey(key);
    }
 
    /**
     * 获得缓存的基本对象。
     *
     * @param key 缓存键值
     * @return 缓存键值对应的数据
     */
    public <T> T getCacheObject(final String key)
    {
        ValueOperations<String, T> operation = redisTemplate.opsForValue();
        return operation.get(key);
    }
 
    /**
     * 删除单个对象
     *
     * @param key
     */
    public boolean deleteObject(final String key)
    {
        return redisTemplate.delete(key);
    }
 
    /**
     * 删除集合对象
     *
     * @param collection 多个对象
     * @return
     */
    public boolean deleteObject(final Collection collection)
    {
        return redisTemplate.delete(collection) > 0;
    }
 
    /**
     * 缓存List数据
     *
     * @param key 缓存的键值
     * @param dataList 待缓存的List数据
     * @return 缓存的对象
     */
    public <T> long setCacheList(final String key, final List<T> dataList)
    {
        Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
        return count == null ? 0 : count;
    }
 
    /**
     * 获得缓存的list对象
     *
     * @param key 缓存的键值
     * @return 缓存键值对应的数据
     */
    public <T> List<T> getCacheList(final String key)
    {
        return redisTemplate.opsForList().range(key, 0, -1);
    }
 
    /**
     * 缓存Set
     *
     * @param key 缓存键值
     * @param dataSet 缓存的数据
     * @return 缓存数据的对象
     */
    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;
    }
 
    /**
     * 获得缓存的set
     *
     * @param key
     * @return
     */
    public <T> Set<T> getCacheSet(final String key)
    {
        return redisTemplate.opsForSet().members(key);
    }
 
    /**
     * 缓存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);
        }
    }
 
    /**
     * 获得缓存的Map
     *
     * @param key
     * @return
     */
    public <T> Map<String, T> getCacheMap(final String key)
    {
        return redisTemplate.opsForHash().entries(key);
    }
 
    /**
     * 往Hash中存入数据
     *
     * @param key Redis键
     * @param hKey Hash键
     * @param value 值
     */
    public <T> void setCacheMapValue(final String key, final String hKey, final T value)
    {
        redisTemplate.opsForHash().put(key, hKey, value);
    }
 
    /**
     * 获取Hash中的数据
     *
     * @param key Redis键
     * @param hKey Hash键
     * @return Hash中的对象
     */
    public <T> T getCacheMapValue(final String key, final String hKey)
    {
        HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();
        return opsForHash.get(key, hKey);
    }
 
    /**
     * 获取多个Hash中的数据
     *
     * @param key Redis键
     * @param hKeys Hash键集合
     * @return Hash对象集合
     */
    public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys)
    {
        return redisTemplate.opsForHash().multiGet(key, hKeys);
    }
 
    /**
     * 删除Hash中的某条数据
     *
     * @param key Redis键
     * @param hKey Hash键
     * @return 是否成功
     */
    public boolean deleteCacheMapValue(final String key, final String hKey)
    {
        return redisTemplate.opsForHash().delete(key, hKey) > 0;
    }
 
    /**
     * 获得缓存的基本对象列表
     *
     * @param pattern 字符串前缀
     * @return 对象列表
     */
    public Collection<String> keys(final String pattern)
    {
        return redisTemplate.keys(pattern);
    }
 
    /**
     * 存储有序集合
     * @param key 键
     * @param value 值
     * @param score 排序
     */
    public void zSet(Object key, Object value, double score){
        redisTemplate.opsForZSet().add(key, value, score);
    }
 
    /**
     * 存储值
     * @param key 键
     * @param set 集合
     */
    public void zSet(Object key, Set set){
        redisTemplate.opsForZSet().add(key, set);
    }
 
    /**
     * 获取key指定范围的值
     * @param key 键
     * @param start 开始位置
     * @param end 结束位置
     * @return 返回set
     */
    public Set zGet(Object key, long start, long end){
        Set set = redisTemplate.opsForZSet().range(key, start, end);
        return set;
    }
 
    /**
     * 获取key对应的所有值
     * @param key 键
     * @return 返回set
     */
    public Set zGet(Object key){
        Set set = redisTemplate.opsForZSet().range(key, 0, -1);
        return set;
    }
 
    /**
     * 获取对用数据的大小
     * @param key 键
     * @return 键值大小
     */
    public long zGetSize(Object key){
        Long size = redisTemplate.opsForZSet().size(key);
        return size;
    }
}

4.10, SecurityConfig core configuration class

This is the core code, and the comments are all on the code. You can take a closer look at it yourself, so I won’t elaborate here.

Full code:

package com.example.security.config;

import com.example.security.filter.JwtAuthenticationTokenFilter;
import com.example.security.service.serviceImpl.AuthenticationEntryPointImpl;
import com.example.security.service.serviceImpl.LogoutSuccessHandlerImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.web.filter.CorsFilter;

import javax.annotation.Resource;

@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Resource
    private UserDetailsService userDetailsService;

    /**
     * token认证过滤器
     */
    @Resource
    private JwtAuthenticationTokenFilter authenticationTokenFilter;

    /**
     * 认证失败处理类
     */
    @Resource
    private AuthenticationEntryPointImpl unauthorizedHandler;

    /**
     * 退出处理类
     */
    @Resource
    private LogoutSuccessHandlerImpl logoutSuccessHandler;

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

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
                // CSRF禁用,因为不使用session
                .csrf().disable()
                // 认证失败处理类
                .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
                // 基于token,所以不需要session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                // 过滤请求
                .authorizeRequests()
                //允许登录接口匿名访问
                .antMatchers("/login").permitAll()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated()
                .and()
                .headers().frameOptions().disable();
        // 添加JWT filter
        httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        // 添加Logout filter
        httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
    }

    /**
     * 强散列哈希加密实现
     */
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder()
    {
        return new BCryptPasswordEncoder();
    }


    /**
     * 身份认证接口
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception
    {
        auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
    }
}

4.11, AuthenticationContextHolder thread local storage

It mainly provides the following static methods:

1. getContext() obtains the Authentication object of the current thread.

2. setContext (Authentication context) sets the Authentication object of the current thread.

3. clearContext() clears the Authentication object of the current thread.

It uses ThreadLocal to maintain thread isolation, so each thread has its own Authentication information without interfering with each other.

In Spring Security, authentication information can be passed at different layers through this class.

key code:

public class AuthenticationContextHolder
{
    private static final ThreadLocal<Authentication> contextHolder = new ThreadLocal<>();

    public static Authentication getContext()
    {
        return contextHolder.get();
    }

    public static void setContext(Authentication context)
    {
        contextHolder.set(context);
    }

    public static void clearContext()
    {
        contextHolder.remove();
    }
}

4.12. UserServiceImpl query user interface

Here I am lazy and did not connect to the database. This password is obtained through encryption with the following code.

key code:

PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String encode = passwordEncoder.encode("123456");
System.out.println(encode);

It is mainly for demonstration, so the user information and role information are directly written here, and the actual development still needs to connect to the database.

key code:

@Service
public class UserServiceImpl {

    public User selectUserByUsername(String username){
        User user = new User();
        user.setId(UUID.randomUUID().toString());
        user.setUsername(username);
        user.setPassword("$2a$10$ErrO7WgkEBAWVQwuJtbBve7R2.pSKUrfs7zt8XkASqJKqcetMvAUC");
        Set<Role> roles = new HashSet<>();
        Role role1 = new Role(1L, "ROLE_ADMIN");
        Role role2 = new Role(2L, "ROLE_USER");
        roles.add(role1);
        roles.add(role2);
        user.setRoles(roles);
        return user;
    }
}

Here users must use ROLE_ as a prefix. For specific analysis, see the @PreAuthorize annotation analysis in 3.15 .

4.13, PasswordServiceImpl password verification service class 

1. The validate() method is used to verify the user password.

2. It first obtains the currently authenticated user name and password from the AuthenticationContextHolder.

3. Then call the matches() method to verify the password.

4. The matches() method uses BCryptPasswordEncoder to perform matching verification on the stored ciphertext password.

5. If the matching is successful, the verification is successful, and if it fails, the verification fails.

In this way, the information of the current authentication principal can be obtained through the AuthenticationContextHolder of Spring Security.

Combined with password encryption matching verification, password verification can be conveniently implemented in the service.

key code:

@Service
public class PasswordServiceImpl {

    public void validate(User user)
    {
        Authentication usernamePasswordAuthenticationToken = AuthenticationContextHolder.getContext();
        String AuthUsername = usernamePasswordAuthenticationToken.getName();
        String AuthPassword = usernamePasswordAuthenticationToken.getCredentials().toString();

        if (matches(user, AuthPassword)) {
            System.out.println("验证成功!");
        } else {
            System.out.println("验证失败!");
        }
    }

    public boolean matches(User user, String rawPassword)
    {
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        return passwordEncoder.matches(rawPassword, user.getPassword());
    }
}

4.14, UserDetailsServiceImpl authentication user service class

1. Implement the UserDetailsService interface of Spring Security.

UserDetailsService is the core interface used by Spring Security to load user information. Custom implementations can flexibly control the user information loading process.

2. Load user information according to user name. Query the database through userService to obtain user objects, including user information such as user name, password, role, etc.

3. Verify user password Use passwordService for password verification to check whether the login password is correct.

4. Construct user authority information and convert user role information into GrantedAuthority authorization information collection.

5. Encapsulate user object return Encapsulate user information and permission information into LoginUser object and return as UserDetails.

6. Provide user details during login verification. Spring Security will call this service to obtain user details during login verification for authentication and authorization.

key code:

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Resource
    private PasswordServiceImpl passwordService;

    @Resource
    private UserServiceImpl userService;

    @Override
    public UserDetails loadUserByUsername(String username) {
        User user = userService.selectUserByUsername(username);
        passwordService.validate(user);
        //取出角色和权限信息
        Set<Role> roles = user.getRoles();

        Set<GrantedAuthority> authorities = new HashSet<>();
        for (Role role : roles) {
            authorities.add(new SimpleGrantedAuthority(role.getName()));
        }
        return new LoginUser(user,authorities);
    }

}

4.15, LoginController login interface

All logins must go through this interface. After successful login, a token will be returned to the user to access the protected resources of the system.

key code: 

@RestController
public class LoginController {

    @Resource
    private LoginServiceImpl loginService;

    @PostMapping("/login")
    public AjaxResult login(@RequestBody LoginBody loginBody)
    {
        AjaxResult ajax = AjaxResult.success();
        // 生成令牌
        String token = loginService.login(loginBody.getUsername(), loginBody.getPassword());
        ajax.put(Constants.TOKEN, token);
        return ajax;
    }


}

4.16, LoginServiceImpl login interface core logic

1. Create UsernamePasswordAuthenticationToken, which contains username and password.

2. Set the authenticationToken to AuthenticationContextHolder, which is a holder for storing authentication provided by Spring Security.

3. Call the authenticate method of AuthenticationManager for authentication. This method will call related UserDetailsService according to the configuration for authentication. ( The loadUserByUsername method in the authentication user information service class will be followed below ), and an authentication object will be returned after the verification is successful.

4. Clear the AuthenticationContextHolder.

5. Obtain the login user information LoginUser from the authentication object.

6. Use TokenService to generate JWT token.

7. Return the JWT token.

key code:

@Service
public class LoginServiceImpl {

    @Resource
    private AuthenticationManager authenticationManager;

    @Resource
    private TokenService tokenService;

    public String login(String username, String password)
    {
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
        AuthenticationContextHolder.setContext(authenticationToken);
        Authentication authentication = authenticationManager.authenticate(authenticationToken);
        AuthenticationContextHolder.clearContext();
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        return tokenService.createToken(loginUser);
    }

}

4.17, LogoutSuccessHandlerImpl logout core logic 

LogoutSuccessHandlerImpl implements the LogoutSuccessHandler interface of Spring Security, which is used to handle the logic after the user logs out successfully.

1. Obtain the currently logged in user information LoginUser through TokenService.

2. If LoginUser is not empty, call the delLoginUser method of TokenService to delete the user's cache information. This involves a custom TokenService for handling token-based user authentication. A token corresponding to the user information is generated when logging in, and the cache relationship needs to be deleted when logging out.

3. Use ServletUtils to write a successful exit prompt to the response, so that the ajax request can obtain the prompt information.

key code:

/**
 * 退出登录
 */
@Configuration
public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler
{
    @Autowired
    private TokenService tokenService;

    /**
     * 退出处理
     * @return
     */
    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
    {
        LoginUser loginUser = tokenService.getLoginUser(request);
        if (StringUtils.isNotNull(loginUser))
        {
            // 删除用户缓存记录
            tokenService.delLoginUser(loginUser.getToken());
        }
        ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.success("退出成功")));
    }
}

4.18. @PreAuthorize annotation

This annotation is placed on the interface to mark that only specified users have access to the interface.

1. When using the @PreAuthorize("hasRole('USER')") annotation on the interface , I can analyze the internal source code execution process for you.

First get the interface parameter configured above as USER .

Enter the hasAnyAuthorityName method to get all the permissions of the current user ( roleSet collection ), and then use the for loop to traverse the parameter array on the annotation. This cycle is only once, because there is only one USER on the annotation , so the length of the array is 1. If If the permission information of the current interface is included in all permission sets of the current user, it will be allowed, otherwise it will not be released. 

3. The getRoleWithDefaultPrefix method is used to detect whether the currently passed in role is prefixed with the default ROLE_ , if so, it will be returned directly, if not, it will be returned with the default prefix of ROLE_ .

This is the verification process of this annotation, which also explains why user permissions must be prefixed with ROLE_ in 3.9.

4.19, HelloController test interface

Write two test interfaces respectively, one is an interface that anyone can access after login, one is an interface that can only be accessed by users who need USER permission after login, and the other is an interface that can only be accessed after login that requires COMMON permission.

key code:

@RestController
public class HelloController {

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

    @GetMapping("/user")
    @PreAuthorize("hasRole('USER')")
    public String user(){
        return "Hello USER!";
    }

    @GetMapping("/common")
    @PreAuthorize("hasRole('COMMON')")
    public String common(){
        return "Hello COMMON!";
    }
}

Note: It must be modified with a public method, otherwise the @PreAuthorize annotation will be invalid!

4.20 Summary

Finally, we summarize the Spring Security authentication process through actual projects:

1. In the login method of LoginService, construct a UsernamePasswordAuthenticationToken, which contains the username and password, which is the entry point for authentication.

2. LoginService calls the authenticate method of AuthenticationManager to start the authentication process.

3. AuthenticationManager will find a matching AuthenticationProvider for authentication.

4. AuthenticationProvider will call the loadUserByUsername method of UserDetailsService to load user information. Here we query users through UserDetailsServiceImpl.

5. In UserDetailsServiceImpl, query user information according to username, and then call PasswordService for password verification.

6. PasswordService obtains the login user name and password through AuthenticationContextHolder. Then match with the user password (encoded) stored in the database, and if it matches, the verification is successful.

7. After the PasswordService authentication is successful, UserDetailsServiceImpl will construct a UserDetails object (here is LoginUser) according to the user information, including the user name, password, permission information, etc.

8. UserDetailsServiceImpl returns UserDetails to AuthenticationProvider.

9. After the AuthenticationProvider receives the UserDetails, it completes the verification and generates an authenticated Authentication object.

10. AuthenticationProvider returns Authentication to AuthenticationManager.

11. The AuthenticationManager sets the Authentication to the SecurityContextHolder for subsequent access control.

12. LoginService gets the authenticated Authentication, extracts UserDetails from it, generates JWTtoken and returns it.

To sum up, combined with the logic of the project, the authentication process of Spring Security can be roughly divided into: obtaining user information -> user verification -> building UserDetails -> generating Authentication. We implemented user authentication logic by customizing UserDetailsService and PasswordService. 

5. Run the project 

5.1. Successful login

Send data in json format through post request for login.

The login is successful and the Token token is returned!

5.2. Accessing Unauthorized Interfaces

Set the token you just obtained into the request header to access the test interface.

5.3. Access to interfaces that require USER authority

Change hello to user.

Because this user is configured with two permissions of ADMIN and USER by default, the access can be successful! 

5.4. Access to interfaces that require COMMON permissions

Change user to common.

Obviously, a 403 error message was returned. 

5.5. Log out

Change common to logout.

Sign out successfully.

5.6. Access failure 

Log out above, and see if you can still access the test interface by carrying the previous token.

It can be clearly seen that the system returns a 401 error. 

5.7. Login failed

We are all examples of successful login above. We deliberately entered the wrong password this time to see if we can log in successfully.

It can be clearly seen that the system returns a 401 error. 

6. Gitee source code address

Because the code provided in this blog is not complete, I open sourced the complete project to Code Cloud for your study and reference!

Project address: SpringBoot integrates SpringSecurity+JWT+Redis complete tutorial

7. Summary

The above is my personal understanding of Spring Security and how to develop applications in actual projects. If you have any questions, please leave a message in the comment area!

Guess you like

Origin blog.csdn.net/HJW_233/article/details/131969622