springSecurity+JWT+Redis authority authentication (complete project code)

1. Understanding spring Security

springSecurity is a security framework for authorization authentication, which is the same as shiro on the market, except that it is more complex, more flexible, and easier to expand than shiro. All security frameworks basically do two things:

1. Authentication: The so-called authentication simply means that you must log in with a user and password before you can enter the page

2. Permission: The so-called permission refers to whether you have permission to operate the current page after you enter the page. If you are the publisher of this article, you have the right to modify the content; ordinary users can only read this article, but not modify it.

In fact, in our work, we don't necessarily need to use the security authentication framework provided by the market, but a set of authority authentication logic written by ourselves, but no matter whether a third-party security framework is used in the project, the process principle is actually the same. It is the same, except that the framework provided by the third party may be more versatile and scalable. After we learn springSecurity, whether you use it in your project is still very helpful for personal improvement.

JWT: It is easy to understand the algorithm that encrypts user information into strings. It can be simply understood that it is the same as uuid, except that the string of uuid is meaningless, while the string generated by jwt is meaningful. After parsing, you can get the stored user information

redis: It is purely to reduce the pressure on the database, and does not need to access the database for authentication and authorization every time the interface is called.

Second, the simple use of springSecurity

First create a springboot project, introduce the springSecurity dependency in the pom file, and then access the interface you wrote, you will jump to the login page provided by springSecurity, the account is user, and the password can be seen in the startup console, and then Enter the account password to access the interface. By the way, spring security also provides a default logout page. http://localhost:8080/logout

insert image description here

insert image description here

3. Understand the authentication process of springSecurity

Obviously, in actual work, it is impossible for us to use the default login and logout pages to log in and log out. In our own projects, the front end will have login and logout pages. So obviously we still need to configure security, instead of using the default configuration. Of course, if we want to configure security, we need to understand the entire process of security in order to write related configurations.

The authentication and authorization process of spring security is a filter chain, of which we mainly focus on three. In fact, in general projects, we mainly implement the interface rewriting methods in the three filters.

UsernamePasswordAuthenticationFilter : the filter used by the user to log in

ExceptionTranslationFilter : Exception handling for authentication failure or authorization failure.

FilterSecurityInterceptor : the filter used in the authorization process

insert image description here

Fourth, the detailed use process of springsecurity

Here only the code necessary for the process is extracted, the complete project code and the sql of the related table, I put it on github, and I will put the url address at the end

1. Define the login interface, and use the authenticate interface of authenticationManager for user authentication.

The AuthenticationManager needs to be injected into the spring container to call method authentication when the user logs in. In addition, spring security will encrypt the plain text of the front-end password and perform password verification, so we have to inject the BCryptPasswordEncoder class into the spring container, otherwise the verification will fail.

@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
    return super.authenticationManagerBean();
}
@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}
//使用AuthenticationManager的认证接口惊醒用户认证
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(query.getUserName(), query.getPassword());
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
if (authenticate == null) {
    
    
    throw new CustomException("登录失败");
}

Note: The authentication method called in this step needs to be rewritten, so after this step, implement the UserDetailsService interface, rewrite the loadUserByUsername method in it, and encapsulate the user's basic information and permission information into Authentication and return.

@Override
    public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
    
    
        // 查询用户认证信息
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<User>()
                .eq(User::getUserName, userName);
        User user = userMapper.selectOne(wrapper);
        if (user == null) {
    
    
            throw new CustomException("用户名或者密码错误");
        }

//        List<String> permissions = ListUtil.toLinkedList("test", "admin");
        // 查询用户权限信息
        List<UserPermissionDto> list = menuMapper.getPermissionsByUserId(user.getUserId());
        List<String> permissions = null;
        if (CollUtil.isNotEmpty(list)) {
    
    
            permissions = list.stream().map(UserPermissionDto::getPerms)
                    .collect(Collectors.toList());
        }
        // 封装UserDetails
        LoginUser loginUser = new LoginUser(user, permissions);
        return loginUser;
    }

The information encapsulated by LoginUser needs to implement the UserDetails interface, where springsecurity uses the factory method pattern to create objects.

package com.example.security.model.vo;

import cn.hutool.core.collection.CollUtil;
import com.alibaba.fastjson.annotation.JSONField;
import com.example.security.model.entity.User;
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 GoryLee
 * @Date 2022/12/5 01:57
 * @Version 1.0
 */
@Data
public class LoginUser implements UserDetails {
    
    

    private User user;

    private List<String> permissions;

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

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

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
    
    
        if(CollUtil.isNotEmpty(authorities)){
    
    
            return authorities;
        }

        return permissions.stream()
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
    }

    @Override
    public String getPassword() {
    
    
        return user != null ? user.getPassword() : null;
    }

    @Override
    public String getUsername() {
    
    
        return user != null ? user.getUserName() : null;
    }

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

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

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

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

Then, we return to the login interface. If the authentication is successful, we need to generate a token through JWT and return it to the front-end. After that, every time the front-end calls the back-end interface, it needs to carry this token for verification. In addition, we also need to store the user's basic information and permission information in redis.

 @PostMapping("/login")
    public Result<Map<String, String>> login(@RequestBody UserBo query) {
    
    

        //使用AuthenticationManager的认证接口惊醒用户认证
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(query.getUserName(), query.getPassword());
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);
        if (authenticate == null) {
    
    
            throw new CustomException("登录失败");
        }
        // 认证成功则通过jwt创建token
        LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
        String token = JwtUtil.createToken(loginUser.getUser().getUserId(), loginUser.getUser().getUserName());
        Map<String, String> data = new HashMap<>();
        data.put("token", token);

        // 把用户信息存入到redis
        String key = "login:" + loginUser.getUser().getUserId();
        if (redisUtil.containsKey(key)) {
    
    
            redisUtil.del(key);
        }
        redisUtil.set(key, loginUser);
//        User user = (User)redisUtil.get("token_" + loginUser.getUser().getId());
        return Result.createSuccess(data);

    }
2. Configuration of spring security

After completing the login interface, we need to make relevant configurations to allow the interface to be released, otherwise, the direct return will still be blocked. The custom class inherits WebSecurityConfigurerAdapter and rewrites the configure(HttpSecurity http) method, including the two beans that need to be injected when logging in above are also defined here. Follow-up filters before authentication, exception handling result filters, and cross-domain are all configured here.

 package com.example.security.config;

import com.example.security.handler.AuthenticationEntryPointHandler;
import com.example.security.handler.CustomAccessDeniedHandler;
import com.example.security.handler.JwtAuthenticationTokenFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

/**
 * @Author GoryLee
 * @Date 2022/12/5 20:24
 * @Version 1.0
 */
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    

    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    @Autowired
    private AuthenticationEntryPointHandler authenticationEntryPointHandler;

    @Autowired
    private CustomAccessDeniedHandler customAccessDeniedHandler;

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

    /**
     *
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
    
        http
                //关闭csrf功能
                .csrf().disable()
                //不通过session获取securityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 允许匿名访问,登录后不能访问
                .antMatchers("/user/login").anonymous()
                // 无论登录还是未登录都能够访问
                .antMatchers("/user/addUser").permitAll()
                // 除了上面的路径,其他都需要鉴权访问
                .anyRequest().authenticated();

        // 添加过滤器
        http
                .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

        //异常处理
        http
                .exceptionHandling()
                // 认证异常处理器
                .authenticationEntryPoint(authenticationEntryPointHandler)
                // 授权异常处理器
                .accessDeniedHandler(customAccessDeniedHandler);

        // 添加跨域处理
        http
                .cors();

    }

    /**
     * 注入AuthenticationManager到spring容器中,用于用户登录时调用方法验证
     *
     * @return
     * @throws Exception
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
    
    
        return super.authenticationManagerBean();
    }
}

3. Define the filter before interface access, that is, the first step of authentication.

Through the above two steps, we can log in successfully and get the token. Next, we need to carry this token when we access other interfaces, so we need to define a filter, parse the token and authorize user authentication. Inherit OncePerRequestFilter and rewrite the doFilterInternal method.

package com.example.security.handler;

import cn.hutool.core.util.StrUtil;
import com.example.security.exception.CustomException;
import com.example.security.model.vo.LoginUser;
import com.example.security.utils.RedisUtil;
import example.common.utils.JwtUtil;
import io.jsonwebtoken.Claims;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

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

/**
 * token认证过滤器
 * @Author GoryLee
 * @Date 2022/12/6 21:34
 * @Version 1.0
 */
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    
    

    @Autowired
    private RedisUtil redisUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
    
    
        // 获取token
        String token = request.getHeader("token");
        if (StrUtil.isEmpty(token)) {
    
    
            filterChain.doFilter(request, response);
            return;
        }
        // 解析token获取userId
        Long userId;
        try {
    
    
            Claims claims = JwtUtil.parseToken(token);
            userId = claims.get("userId", Long.class);
        } catch (Exception e) {
    
    
            throw new CustomException("非法token");
        }
        // 从redis中获取用户信息
        LoginUser loginUser = redisUtil.get("login:" + userId);
        if(loginUser == null ){
    
    
            throw new CustomException("用户未登录");
        }
        // 存入SecurityContextHolder认证
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(loginUser,null,loginUser.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        filterChain.doFilter(request, response);
    }
}

Configure the filter in the configuration class

				// 添加过滤器
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
4. For interface permission verification, you can use the verification method provided by spring security, and also use our custom verification method

First, add above our custom configuration class

@EnableGlobalMethodSecurity(prePostEnabled = true)

Secondly, we need to add the method of the interface interface, hasAuthority is the method provided by spring security, and sys:admin is the required permission string

@PreAuthorize("hasAuthority('sys:admin')")

Entering SecurityExpressionRoot, we can see the methods provided by springsecurity

[External link picture transfer failed, the source site may have an anti-leeching mechanism, it is recommended to save the picture and upload it directly (img-3SsO2Pu9-1670679608191)(/Users/liguanyu/Documents/picture/typora screenshot/image-20221210210446130.png)]

Of course, we can also define verification rules to verify by ourselves. We only need to define a class of verification method and inject it into the container

package com.example.security.expression;

import cn.hutool.core.util.StrUtil;
import com.example.security.model.vo.LoginUser;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;

import java.util.List;

/**
 * @Author GoryLee
 * @Date 2022/12/7 22:33
 * @Version 1.0
 */
@Component("customExpression")
public class CustomSecurityExpressionRoot {
    
    


    public final boolean hasAuthority(String per) {
    
    
        // 从redis中获取用户权限 或者SecurityContextHolder中获取
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        List<String> permissions = loginUser.getPermissions();
        for (String permission : permissions) {
    
    
            if(StrUtil.contains(permission,per)){
    
    
                return true;
            }
        }
        return false;
    }
}

Then use the sel expression in the interface annotation to use the method, as follows

@PreAuthorize("@customExpression.hasAuthority('sys')")
5. Rewrite the exception result filter and return the exception information to the front end in a unified format

So far, we have completed the two steps of authentication and authorization, but there is still a problem, that is, if the authentication fails or the authorization fails, we will find that the interface returns 500, and the information is a string of English strings, which is not what we want. of the result.

5.1 web tool class, return the response information in a unified format
@Component
@Slf4j
public class WebUtil {
    
    

    /**
     * 认证授权异常处理
     * @param response
     * @param data
     * @return
     */
    public static String renderString(HttpServletResponse response, String data){
    
    
        try {
    
    
            response.setStatus(200);
            response.setContentType("application/json");
            response.setCharacterEncoding("utf-8");
            response.getWriter().println(data);
        } catch (IOException e) {
    
    
            e.printStackTrace();
        }
        return null;
    }

}
5.2 Implement the AuthenticationEntryPoint interface, fail in the authentication process, catch an exception, encapsulate it into a unified response body format and return
package com.example.security.handler;

import cn.hutool.http.HttpStatus;
import com.alibaba.fastjson.JSON;
import com.example.security.utils.WebUtil;
import example.common.model.Result;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

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

/**
 * 认证过程中失败,捕获到异常,封装成统一的响应体格式返回
 * @Author GoryLee
 * @Date 2022/12/7 20:44
 * @Version 1.0
 */
@Component
public class AuthenticationEntryPointHandler implements AuthenticationEntryPoint {
    
    
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response,
                         AuthenticationException authException) throws IOException, ServletException {
    
    
        Result<Object> result = Result.createError(HttpStatus.HTTP_UNAUTHORIZED, "认证失败请重新登录");
        String jsonString = JSON.toJSONString(result);
        WebUtil.renderString(response,jsonString);
    }
}
5.3 Implement the AccessDeniedHandler interface

Note: The exception AccessDeniedException thrown when the interface permission authentication fails inherits RuntimeException. If you use the global exception to catch the RuntimeException, the information captured by the global exception will be returned instead of the custom permission exception information.

package com.example.security.handler;

import cn.hutool.http.HttpStatus;
import com.alibaba.fastjson.JSON;
import com.example.security.utils.WebUtil;
import example.common.model.Result;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

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

/**
 * 授权过程中失败,捕获到异常,封装成统一的响应体格式返回
 * 注意:接口权限认证失败抛出的异常AccessDeniedException继承RuntimeException
 * 如果使用全局异常捕获RuntimeException,会返回全局异常捕获的信息,而不会返回自定义的权限异常信息
 * @Author GoryLee
 * @Date 2022/12/7 20:51
 * @Version 1.0
 */
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
    
    
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
    
    
        Result<Object> result = Result.createError(HttpStatus.HTTP_FORBIDDEN, "授权失败,你没权限操作");
        String jsonString = JSON.toJSONString(result);
        WebUtil.renderString(response,jsonString);
    }
}
5.4 Configure the exception result handler in the configuration class
//异常处理
http
        .exceptionHandling()
        // 认证异常处理器
        .authenticationEntryPoint(authenticationEntryPointHandler)
        // 授权异常处理器
        .accessDeniedHandler(customAccessDeniedHandler);
6. Logout interface (optional)

In an enterprise project, there are two ways to log out of a page

1. Clear the front-end localstorecache and clear the token to complete the logout, which is also in line with the stateless development concept of jwt.

2. Clear the front-end localstorecache, clear the token, and clear the redis cache through the background interface

@GetMapping("/logout")
public Result<String> logout() {
    
    
    UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
    User user = (User)authentication.getPrincipal();
    redisUtil.del("login:"+user.getUserId());
    return Result.createSuccess("注销成功");
}

V. Summary

The above mainly talks about how to use spring security correctly in the enterprise, and the code of the core process has been pasted.

Complete project code URL address: https://gitee.com/gorylee/learnDemo/tree/master/securityDemo

Guess you like

Origin blog.csdn.net/qq798867485/article/details/128269552