SpringSecurity from entry to mastery

Video link: SpringSecurity framework tutorial -Spring Security+JWT implementation of project level front-end separation, authentication and authorization -B site's most accessible Spring Security Course_ Beep beep_ bilibili

brief introduction

Spring Security is a security management framework in the spring family. Compared with Shiro, another security framework, it provides richer functions and community resources.

Generally speaking, large and medium-sized projects use SpringSecurity as a security framework. Shiro has many small projects, because it is easier to get started than SpringSecurity.

General Web applications require authentication and authorization.

Authentication: verify whether the user who currently accesses the system is the user of the system, and confirm the specific user

Authorization: determine whether the current user has permission to perform an operation after authentication

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

1. Get started quickly

1.1 preparation

We need to build a simple SpringBoot project first

① Set parent project add dependency

 <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.0</version>
    </parent>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>

② Create startup class

@SpringBootApplication
public class SecurityApplication {

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

③ Create Controller

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

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

1.2 introducing SpringSecurity

Using SpringSecurity in the SpringBoot project, we only need to introduce dependencies to implement the entry case.

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

After dependency is introduced, the interface before we try to access will automatically jump to a default login page of SpringSecurity. The default user name is user, and the password will be output on the console.

The interface can only be accessed after logging in.

2. Certification

2.1 login verification process

2.2 principle discussion

If you want to know how to implement your login process, you must first know the SpringSecurity process in the entry case.

2.2.1 SpringSecurity complete process

The principle of SpringSecurity is actually a filter chain, which contains filters that provide various functions. Here we can take a look at the filter in the introductory case.

Only core filters are shown in the figure, and other non core filters are not shown in the figure.

UsernamePasswordAuthenticationFilter: is responsible for processing the login request after we fill in the user name and password on the login page. It is mainly responsible for the certification of entry cases.

ExceptionTranslationFilter: handles any AccessDeniedException and AuthenticationException thrown in the filter chain.

FilterSecurityInterceptor: a filter responsible for permission verification.

We can check which filters are in the SpringSecurity filter chain in the current system and their order through Debug.

2.2.2 detailed explanation of certification process

 

Concept quick check:

Authentication interface: its implementation class represents the current user accessing the system and encapsulates user related information.

AuthenticationManager interface: defines the method of authenticating Authentication

UserDetailsService interface: the core interface for loading user specific data. It defines a method to query user information according to user name.

UserDetails interface: provides core user information. The user information obtained and processed by UserDetailsService according to the user name should be encapsulated into a UserDetails object and returned. This information is then encapsulated in the Authentication object.

2.3 problem solving

2.3.1 thinking analysis

Sign in

① Custom login interface

Call the method of ProviderManager to authenticate. If the authentication passes, generate jwt

Store user information in redis

② Customize UserDetailsService

Query the database in this implementation class

Verification:

① Define Jwt authentication filters

Get token

Parse the token to get the userid

Get user information from redis

Deposit into SecurityContextHolder

2.3.2 preparations

① Add dependency

 <!--redis rely on-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!--fastjson rely on-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.33</version>
        </dependency>
        <!--jwt rely on-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.0</version>
        </dependency>

② Add Redis related configuration

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 Using FastJson serialization
 * 
 * @author sg
 */
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);
    }
}
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 also adopts the serialization method of StringRedisSerializer
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(serializer);

        template.afterPropertiesSet();
        return template;
    }
}

③ Response class

import com.fasterxml.jackson.annotation.JsonInclude;

/**
 * @Author
 */
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ResponseResult<T> {
    /**
     * Status code
     */
    private Integer code;
    /**
     * Prompt information. If there is an error, the front end can get this field to prompt
     */
    private String msg;
    /**
     * The query 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;
    }
}

④ Tools

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 Tools
 */
public class JwtUtil {

    //Valid for
    public static final Long JWT_TTL = 60 * 60 *1000L;// 60 * 60 * 1000 one hour
    //Set secret key plaintext
    public static final String JWT_KEY = "sangeng";

    public static String getUUID(){
        String token = UUID.randomUUID().toString().replaceAll("-", "");
        return token;
    }
    
    /**
     * Generate jtw
     * @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
     * @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();
    }

    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)   // Topics can be JSON data
                .setIssuer("sg")     // Issued by
                .setIssuedAt(now)      // Time filed 
                .signWith(signatureAlgorithm, secretKey) //Use HS256 symmetric encryption algorithm for signature, and the second parameter is the secret key
                .setExpiration(expDate);
    }

    /**
     * Create a token
     * @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();
    }

    public static void main(String[] args) throws Exception {
        String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJjYWM2ZDVhZi1mNjVlLTQ0MDAtYjcxMi0zYWEwOGIyOTIwYjQiLCJzdWIiOiJzZyIsImlzcyI6InNnIiwiaWF0IjoxNjM4MTA2NzEyLCJleHAiOjE2MzgxMTAzMTJ9.JVsSbkP94wuczb4QryQbAke3ysBDIL5ou8fWsbt_ebg";
        Claims claims = parseJWT(token);
        System.out.println(claims);
    }

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


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

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

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

    /**
     * Cache basic objects, such as Integer, String, entity class, etc
     *
     * @param key Cached key value
     * @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 effective time
     *
     * @param key Redis key
     * @param timeout Timeout
     * @return true=Set successfully; false= setting failed
     */
    public boolean expire(final String key, final long timeout)
    {
        return expire(key, timeout, TimeUnit.SECONDS);
    }

    /**
     * Set effective time
     *
     * @param key Redis key
     * @param timeout Timeout
     * @param unit Time unit
     * @return true=Set successfully; false= setting failed
     */
    public boolean expire(final String key, final long timeout, final TimeUnit unit)
    {
        return redisTemplate.expire(key, timeout, unit);
    }

    /**
     * Get the cached base object.
     *
     * @param key Cache key value
     * @return Cache the data corresponding to the key value
     */
    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 value
     * @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 value
     * @return Cache the data corresponding to the key value
     */
    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 that cache 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
     *
     * @param key
     * @return
     */
    public <T> Map<String, T> getCacheMap(final String key)
    {
        return redisTemplate.opsForHash().entries(key);
    }

    /**
     * Store data into 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 in Hash
     * 
     * @param key
     * @param hkey
     */
    public void delCacheMapValue(final String key, final String hkey)
    {
        HashOperations hashOperations = redisTemplate.opsForHash();
        hashOperations.delete(key, hkey);
    }

    /**
     * Get data in multiple hashes
     *
     * @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);
    }
}
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class WebUtils
{
    /**
     * Render string to client
     * 
     * @param response Render objects
     * @param string String to render
     * @return null
     */
    public static String renderString(HttpServletResponse response, String string) {
        try
        {
            response.setStatus(200);
            response.setContentType("application/json");
            response.setCharacterEncoding("utf-8");
            response.getWriter().print(string);
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
        return null;
    }
}

⑤ Entity class

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


/**
 * User entity class
 *
 * @author third night watch
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {
    private static final long serialVersionUID = -40356785423868312L;
    
    /**
    * Primary key
    */
    private Long id;
    /**
    * user name
    */
    private String userName;
    /**
    * nickname
    */
    private String nickName;
    /**
    * password
    */
    private String password;
    /**
    * Account status (0 normal 1 disabled)
    */
    private String status;
    /**
    * mailbox
    */
    private String email;
    /**
    * cell-phone number
    */
    private String phonenumber;
    /**
    * User gender (0 male, 1 female, 2 unknown)
    */
    private String sex;
    /**
    * head portrait
    */
    private String avatar;
    /**
    * User type (0 administrator, 1 ordinary user)
    */
    private String userType;
    /**
    * User id of the Creator
    */
    private Long createBy;
    /**
    * Creation time
    */
    private Date createTime;
    /**
    * Updated by
    */
    private Long updateBy;
    /**
    * Update time
    */
    private Date updateTime;
    /**
    * Delete flag (0 means not deleted, 1 means deleted)
    */
    private Integer delFlag;
}

2.3.3 realization

2.3.3.1 database verification user

From the previous analysis, we can know that we can customize a UserDetailsService and let SpringSecurity use our UserDetailsService. Our own UserDetailsService can query the user name and password from the database.

preparation

Let's create a user table first, and the statement is as follows:

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 disabled)',
  `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 male, 1 female, 2 unknown)',
  `avatar` VARCHAR(128) DEFAULT NULL COMMENT 'head portrait',
  `user_type` CHAR(1) NOT NULL DEFAULT '1' COMMENT 'User type (0 administrator, 1 ordinary user)',
  `create_by` BIGINT(20) DEFAULT NULL COMMENT 'User of the Creator id',
  `create_time` DATETIME DEFAULT NULL COMMENT 'Creation time',
  `update_by` BIGINT(20) DEFAULT NULL COMMENT 'Updated by',
  `update_time` DATETIME DEFAULT NULL COMMENT 'Update time',
  `del_flag` INT(11) DEFAULT '0' COMMENT 'Delete flag (0 means not deleted, 1 means deleted)',
  PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='User table'

Introduce the dependency of MybatisPuls and mysql driver

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

Configure database information

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

Define Mapper interface

public interface UserMapper extends BaseMapper<User> {
}

Modify User entity class

Class name plus@TableName(value = "sys_user") ,id Field plus @TableId

Configure Mapper scan

@SpringBootApplication
@MapperScan("com.sangeng.mapper")
public class SimpleSecurityApplication {
    public static void main(String[] args) {
        ConfigurableApplicationContext run = SpringApplication.run(SimpleSecurityApplication.class);
        System.out.println(run);
    }
}

Add junit dependency

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

Test whether MP can be used normally

/**
 * @Author third night watch 
@SpringBootTest
public class MapperTest {

    @Autowired
    private UserMapper userMapper;

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

Core code implementation

Create a class to implement the UserDetailsService interface and override its methods. Query user information from the database by user name

/**
 * @Author
 */
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //Query user information according to user name
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(User::getUserName,username);
        User user = userMapper.selectOne(wrapper);
        //If the data cannot be queried, an exception is thrown to give a prompt
        if(Objects.isNull(user)){
            throw new RuntimeException("Wrong user name or password");
        }
        //TODO is added to LoginUser according to user query permission information
        
        //Encapsulated as UserDetails object and returned 
        return new LoginUser(user);
    }
}

Because the return value of the UserDetailsService method is of UserDetails type, it is necessary to define a class to implement the interface and encapsulate the user information in it.

/**
 * @Author third night watch
 */
@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();
    }

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

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

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

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

Note: if you want to test, you need to write user data into 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. for example

In this way, you can log in with sg as your user name and 1234 as your password.

2.3.3.2 password encrypted storage

In the actual project, we will not store the password plaintext in the database.

The default PasswordEncoder requires that the password format in the database be: {id}password. It will judge the encryption method of the password according to the ID. But we usually don't use this method. Therefore, PasswordEncoder needs to be replaced.

We usually use the BCryptPasswordEncoder provided by SpringSecurity.

We only need to inject the BCryptPasswordEncoder object into the Spring container, and SpringSecurity will use the PasswordEncoder for password verification.

We can define a SpringSecurity configuration class. SpringSecurity requires this configuration class to inherit WebSecurityConfigurerAdapter.

/**
 * @Author third night watch  
 */
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {


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

}

2.3.3.3 login interface

Next, we need to customize the login interface, and then let SpringSecurity release this interface, so that users can access this interface without logging in.

In the interface, we use the authenticate method of AuthenticationManager to authenticate users, so we need to configure the AuthenticationManager into the container in SecurityConfig.

If the authentication is successful, a jwt should be generated and returned in the response. And in order to identify the specific user through jwt when the user requests next, we need to store the user information in redis, and the user id can be used as the key.

@RestController
public class LoginController {

    @Autowired
    private LoginServcie loginServcie;

    @PostMapping("/user/login")
    public ResponseResult login(@RequestBody User user){
        return loginServcie.login(user);
    }
}
/**
 * @Author third night watch 
 */
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {


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

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //Turn off csrf
                .csrf().disable()
                //Do not get SecurityContext through Session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // Allow anonymous access to login interface
                .antMatchers("/user/login").anonymous()
                // All requests except the above require authentication
                .anyRequest().authenticated();
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}
@Service
public class LoginServiceImpl implements LoginServcie {

    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private RedisCache redisCache;

    @Override
    public ResponseResult login(User user) {
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword());
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);
        if(Objects.isNull(authenticate)){
            throw new RuntimeException("Wrong user name or password");
        }
        //Generate token using userid
        LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
        String userId = loginUser.getUser().getId().toString();
        String jwt = JwtUtil.createJWT(userId);
        //authenticate into redis
        redisCache.setCacheObject("login:"+userId,loginUser);
        //Respond the token to the front end
        HashMap<String,String> map = new HashMap<>();
        map.put("token",jwt);
        return new ResponseResult(200,"Login successful",map);
    }
}

2.3.3.4 certified filters

We need to customize a filter, which will get the token in the request header, parse the token and get the userid in it.

Use userid to get the corresponding LoginUser object in redis.

Then the Authentication object is encapsulated and stored in the SecurityContextHolder

@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;
        }
        //Parse 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 is not logged in");
        }
        //Deposit into SecurityContextHolder
        //TODO obtains permission information and encapsulates it into Authentication
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(loginUser,null,null);
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        //Release
        filterChain.doFilter(request, response);
    }
}
/**
 * @Author third night watch  
 */
@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
                //Turn off csrf
                .csrf().disable()
                //Do not get SecurityContext through Session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // Allow anonymous access to login interface
                .antMatchers("/user/login").anonymous()
                // All requests except the above require authentication
                .anyRequest().authenticated();

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

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

2.3.3.5 exit login

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

/**
 * @Author third night watch 
 */
@Service
public class LoginServiceImpl implements LoginServcie {

    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private RedisCache redisCache;

    @Override
    public ResponseResult login(User user) {
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword());
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);
        if(Objects.isNull(authenticate)){
            throw new RuntimeException("Wrong user name or password");
        }
        //Generate token using userid
        LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
        String userId = loginUser.getUser().getId().toString();
        String jwt = JwtUtil.createJWT(userId);
        //authenticate into redis
        redisCache.setCacheObject("login:"+userId,loginUser);
        //Respond the token to the front end
        HashMap<String,String> map = new HashMap<>();
        map.put("token",jwt);
        return new ResponseResult(200,"Login successful",map);
    }

    @Override
    public ResponseResult logout() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        Long userid = loginUser.getUser().getId();
        redisCache.deleteObject("login:"+userid);
        return new ResponseResult(200,"Exit successful");
    }
}

3. Authorization

3.0 role of permission system

For example, in a school library management system, if an ordinary student logs in, he can see the functions related to borrowing and returning books. It is impossible for him to see and use the functions of adding book information and deleting book information. However, if a librarian logs in with his account, he should be able to see and use the functions of adding book information and deleting book information.

To sum up, different users can use different functions. This is the effect of the permission system.

We can't just rely on the front end to judge the user's permissions to choose which menus and buttons to display. Because if it's just like this, if someone knows the interface address of the corresponding function, they can directly send requests to realize relevant function operations without going through the front end.

Therefore, we also need to judge the user permission in the background to judge whether the current user has the corresponding permission. We must have the required permission to carry out the corresponding operation.

3.1 basic authorization process

In SpringSecurity, the default FilterSecurityInterceptor will be used for permission verification. In the FilterSecurityInterceptor, the Authentication will be obtained from the SecurityContextHolder, and then the permission information will be obtained. Whether the current user has the permissions required to access the current resource.

Therefore, in the project, we only need to save the permission information of the currently logged in user into Authentication.

Then set the permissions required by our resources.

3.2 authorization realization

3.2.1 restrict the permissions required to access resources

SpringSecurity provides us with an annotation based permission control scheme, which is also the main method 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 relevant configuration first.

@EnableGlobalMethodSecurity(prePostEnabled = true)

Then you can use the corresponding annotation@ PreAuthorize

@RestController
public class HelloController {

    @RequestMapping("/hello")
    @PreAuthorize("hasAuthority('test')")
    public String hello(){
        return "hello";
    }
}

3.2.2 package permission information

When we wrote UserDetailsServiceImpl earlier, we said that after querying the user, we also need to obtain the corresponding permission information, which is encapsulated in UserDetails and returned.

First, we directly write the permission information into UserDetails for testing.

We previously defined the implementation class LoginUser of UserDetails, and we need to modify it to encapsulate permission information.

package com.sangeng.domain;

import com.alibaba.fastjson.annotation.JSONField;
import lombok.AllArgsConstructor;
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;

/**
 * @Author 
 */
@Data
@NoArgsConstructor
public class LoginUser implements UserDetails {

    private User user;
        
    //Store permission information
    private List<String> permissions;
    
    
    public LoginUser(User user,List<String> permissions) {
        this.user = user;
        this.permissions = permissions;
    }


    //A collection that stores the permission information required by SpringSecurity
    @JSONField(serialize = false)
    private List<GrantedAuthority> authorities;

    @Override
    public  Collection<? extends GrantedAuthority> getAuthorities() {
        if(authorities!=null){
            return authorities;
        }
        //Convert the permission information of string type in permissions into GrantedAuthority object and store it in authorities
        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;
    }
}

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

package com.sangeng.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.sangeng.domain.LoginUser;
import com.sangeng.domain.User;
import com.sangeng.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.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;

/**
 * @Author
 */
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(User::getUserName,username);
        User user = userMapper.selectOne(wrapper);
        if(Objects.isNull(user)){
            throw new RuntimeException("Wrong user name or password");
        }
        //TODO is added to LoginUser according to user query permission information
        List<String> list = new ArrayList<>(Arrays.asList("test"));
        return new LoginUser(user,list);
    }
}

3.2.3 query permission information from database

3.2.3.1 RBAC permission model

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

 

3.2.3.2 preparation

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 show 1 hide)',
  `status` char(1) DEFAULT '0' COMMENT 'Menu status (0 normal 1 disabled)',
  `perms` varchar(100) DEFAULT NULL COMMENT 'Permission ID',
  `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 disabled)',
  `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 disabled)',
  `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 male, 1 female, 2 unknown)',
  `avatar` varchar(128) DEFAULT NULL COMMENT 'head portrait',
  `user_type` char(1) NOT NULL DEFAULT '1' COMMENT 'User type (0 administrator, 1 ordinary user)',
  `create_by` bigint(20) DEFAULT NULL COMMENT 'User of the Creator id',
  `create_time` datetime DEFAULT NULL COMMENT 'Creation time',
  `update_by` bigint(20) DEFAULT NULL COMMENT 'Updated by',
  `update_time` datetime DEFAULT NULL COMMENT 'Update time',
  `del_flag` int(11) DEFAULT '0' COMMENT 'Delete flag (0 means not deleted, 1 means 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;
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
package com.sangeng.domain;

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 entity class
 *
 * @author makejava
 * @since 2021-11-24 15:30:08
 */
@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 show 1 hide)
    */
    private String visible;
    /**
    * Menu status (0 normal 1 disabled)
    */
    private String status;
    /**
    * Permission ID
    */
    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;
}

3.2.3.3 code implementation

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

So we can first define a mapper, which provides a method to query permission information according to userid.

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.sangeng.domain.Menu;

import java.util.List;

/**
 * @Author Third watch station B: https://space.bilibili.com/663528522
 */
public interface MenuMapper extends BaseMapper<Menu> {
    List<String> selectPermsByUserId(Long id);
}

Especially for custom methods, you need to create corresponding mapper files and define corresponding sql statements

<?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.sangeng.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 mapperXML file in application.yml

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/sg_security?characterEncoding=utf-8&serverTimezone=UTC
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver
  redis:
    host: localhost
    port: 6379
mybatis-plus:
  mapper-locations: classpath*:/mapper/**/*.xml 

Then we can call the mapper's method in UserDetailsServiceImpl to query the permission information and package it into the LoginUser object.

/**
 * @Author 
 */
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private MenuMapper menuMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(User::getUserName,username);
        User user = userMapper.selectOne(wrapper);
        if(Objects.isNull(user)){
            throw new RuntimeException("Wrong user name or password");
        }
        List<String> permissionKeyList =  menuMapper.selectPermsByUserId(user.getId());
//        //Test writing method
//        List<String> list = new ArrayList<>(Arrays.asList("test"));
        return new LoginUser(user,permissionKeyList);
    }
}

4. Custom failure handling

We also hope to return json with the same structure as our interface in case of authentication failure or authorization failure, so that the front end can handle the response uniformly. To realize this function, we need to know the exception handling mechanism of SpringSecurity.

In SpringSecurity, if an exception occurs during the authentication or authorization process, it will be caught by the ExceptionTranslationFilter. In the ExceptionTranslationFilter, you will judge whether the exception occurs due to authentication failure or authorization failure.

If the exception occurs in the authentication process, it will be encapsulated as AuthenticationException, and then the method of AuthenticationEntryPoint object will be called to handle the exception.

If the exception occurs in the authorization process, it will be encapsulated as AccessDeniedException, and then call the method of AccessDeniedHandler object to handle the exception.

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

① Custom implementation class

@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(), "Insufficient permissions");
        String json = JSON.toJSONString(result);
        WebUtils.renderString(response,json);

    }
}
/**
 * @Author
 */
@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(), "Authentication failed, please login again");
        String json = JSON.toJSONString(result);
        WebUtils.renderString(response,json);
    }
}

② Configure to SpringSecurity

Inject the corresponding processor first

 @Autowired
    private AuthenticationEntryPoint authenticationEntryPoint;

    @Autowired
    private AccessDeniedHandler accessDeniedHandler;

Then we can use the method of HttpSecurity object to configure.

        http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint).
                accessDeniedHandler(accessDeniedHandler);

5. Cross domain

For security reasons, browsers must follow the same origin policy when using the XMLHttpRequest object to initiate HTTP requests, otherwise it is a cross domain HTTP request, which is prohibited by default. The same source strategy requires the same source to communicate normally, that is, the protocol, domain name and port number are completely consistent.  

The front-end and back-end projects are separated, and the front-end projects and back-end projects are generally not homologous, so there must be cross domain requests.

So we need to deal with it so that the front end can make cross domain requests.

① First configure SpringBoot and run cross domain requests

@Configuration
public class CorsConfig implements WebMvcConfigurer {

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

② Enable cross domain access of SpringSecurity

Since our resources will be protected by SpringSecurity, we need to let SpringSecurity run cross domain access if we want cross domain access.

 @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //Turn off csrf
                .csrf().disable()
                //Do not get SecurityContext through Session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // Allow anonymous access to login interface
                .antMatchers("/user/login").anonymous()
                // All requests except the above require authentication
                .anyRequest().authenticated();

        //Add filter
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

        //Configure exception handler
        http.exceptionHandling()
                //Configure authentication failure processor
                .authenticationEntryPoint(authenticationEntryPoint)
                .accessDeniedHandler(accessDeniedHandler);

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

6. Minor problems left

Other permission verification methods

We used the @PreAuthorize annotation before, and then used the hasAuthority method for verification. SpringSecurity also provides us with other methods, such as hasAnyAuthority, hasRole, hasAnyRole, etc.

Here we are not in a hurry to introduce these methods. We should first understand the principle of hasAuthority, and then learn other methods, which will be easier for you to understand, rather than memorizing the differences. And we can also choose to define the verification method to implement our own verification logic.

The hasAuthority method actually implements the hasAuthority of the SecurityExpressionRoot. As long as you debug the breakpoint, you can know its internal verification principle.

Internally, it calls the getAuthorities method of authentication to obtain the user's permission list. Then judge that the method parameter data we saved is in the permission list.

The hasAnyAuthority method can pass in multiple permissions, and only the user with any one of them can access the corresponding resources.

 @PreAuthorize("hasAnyAuthority('admin','test','system:dept:list')")
    public String hello(){
        return "hello";
    }

hasRole requires a corresponding ROLE to access, but it will splice the parameters we passed in * * ROLE_** Compare later. Therefore, in this case, the corresponding permission of the user should also have * * ROLE_** This prefix is OK.

  @PreAuthorize("hasRole('system:dept:list')")
    public String hello(){
        return "hello";
    }

hasAnyRole can be accessed by any ROLE. It will also splice the parameters we passed in * * ROLE_** Compare later. Therefore, in this case, the corresponding permission of the user should also have * * ROLE_** This prefix is OK.

  @PreAuthorize("hasAnyRole('admin','system:dept:list')")
    public String hello(){
        return "hello";
    }

Custom permission verification method

We can also define our own permission verification method and use our method in the @PreAuthorize annotation.

@Component("ex")
public class SGExpressionRoot {

    public boolean hasAuthority(String authority){
        //Get the permissions of the current user
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        List<String> permissions = loginUser.getPermissions();
        //Determine whether authority exists in the user permission set
        return permissions.contains(authority);
    }
}

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

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

Configuration based permission control

We can also use the configuration method to control the permission of resources in the configuration class.

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //Turn off csrf
                .csrf().disable()
                //Do not get SecurityContext through Session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // Allow anonymous access to login interface
                .antMatchers("/user/login").anonymous()
                .antMatchers("/testCors").hasAuthority("system:dept:list222")
                // All requests except the above require authentication
                .anyRequest().authenticated();

        //Add filter
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

        //Configure exception handler
        http.exceptionHandling()
                //Configure authentication failure processor
                .authenticationEntryPoint(authenticationEntryPoint)
                .accessDeniedHandler(accessDeniedHandler);

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

CSRF

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

CSRF attack and defense (well written)_ Catch the thief first catch the king's blog -CSDN blog

SpringSecurity can prevent CSRF attacks through csrf_token. The backend will generate a csrf_token. When the front end initiates a request, it needs to carry this csrf_token, the back-end will have a filter for verification. If it is not carried or forged, access is not allowed.

We can find that CSRF attacks rely on the authentication information carried in cookies. But in the project of front end and back end separation, our authentication information is actually a token, which is not stored in a cookie, and the front-end code needs to set the token into the request header, so there is no need to worry about CSRF attacks.

Authentication successful processor

In fact, when UsernamePasswordAuthenticationFilter performs login authentication, if the login is successful, it will call the AuthenticationSuccessHandler method to process after successful authentication. AuthenticationSuccessHandler is the login success handler.

We can also customize the successful processor to deal with it after success.

@Component
public class SGSuccessHandler implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        System.out.println("Certification successful");
    }
}
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private AuthenticationSuccessHandler successHandler;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin().successHandler(successHandler);

        http.authorizeRequests().anyRequest().authenticated();
    }
}

Authentication failure processor

In fact, when UsernamePasswordAuthenticationFilter performs login authentication, if the authentication fails, it will call the AuthenticationFailureHandler method to handle the authentication failure. AuthenticationFailureHandler is the login failure handler.

We can also customize the failure processor to handle the failure.

@Component
public class SGFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        System.out.println("Authentication failed");
    }
}
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private AuthenticationSuccessHandler successHandler;

    @Autowired
    private AuthenticationFailureHandler failureHandler;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
//                Configure authentication successful processor
                .successHandler(successHandler)
//                Configure authentication failure processor
                .failureHandler(failureHandler);

        http.authorizeRequests().anyRequest().authenticated();
    }
}

Logout successful processor

@Component
public class SGLogoutSuccessHandler implements LogoutSuccessHandler {
    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        System.out.println("Logout successful");
    }
}
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private AuthenticationSuccessHandler successHandler;

    @Autowired
    private AuthenticationFailureHandler failureHandler;

    @Autowired
    private LogoutSuccessHandler logoutSuccessHandler;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
//                Configure authentication successful processor
                .successHandler(successHandler)
//                Configure authentication failure processor
                .failureHandler(failureHandler);

        http.logout()
                //Configuration logoff successful processor
                .logoutSuccessHandler(logoutSuccessHandler);

        http.authorizeRequests().anyRequest().authenticated();
    }
}@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private AuthenticationSuccessHandler successHandler;

    @Autowired
    private AuthenticationFailureHandler failureHandler;

    @Autowired
    private LogoutSuccessHandler logoutSuccessHandler;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
//                Configure authentication successful processor
                .successHandler(successHandler)
//                Configure authentication failure processor
                .failureHandler(failureHandler);

        http.logout()
                //Configuration logoff successful processor
                .logoutSuccessHandler(logoutSuccessHandler);

        http.authorizeRequests().anyRequest().authenticated();
    }
}

Tags: Java Spring Back-end

Posted by khaitan_anuj on Tue, 09 Aug 2022 12:56:56 +0530