How to authenticate Spring Security + token front-end and back-end separation

foreword

Because the learning process of Spring Security is quite tortuous, I thought it was relatively simple at first, but in fact it is relatively simple. The biggest pitfall is that most of the Spring Security found is not based on the configuration of the front-end and back-end separation, which solves a bug. I found more bugs , and I was very annoying until I saw this video on the famous learning website B station. Separation), the videos of the two organizations are almost speechless. I can’t wait to tell you about Spring boot, mysql, and Spring cloud in a security framework, which is really puzzling. If you need to get into the project quickly, this is all summarized, just use it directly! (Version: boot-2.6.3)

The token here is generated using JWT. There are many articles in this area, so I won’t go into details.

Part.1 introduce Security

Because it is the relationship between Spring's pro-son, it is very convenient to introduce, just add dependencies in pom.xml

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

Part.2 Login verification access database

There are many filters (Filter) in Spring security. The login module will call the UsernamePasswordAuthenticationFilter filter (important!) to process the login request. You only need to write a class that inherits UserDetailsService and implements the loadUserByUsername method to perform login verification. If you query the user and Returning UserDetails means passing the verification.

@Service
@RequiredArgsConstructor
public class SysUserDetailService implements UserDetailsService {
    
    

    private final SysUserMapper sysUserMapper;

    @Override
    public UserDetails loadUserByUsername(String account) throws UsernameNotFoundException {
    
    
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        // 从数据库中取出用户信息
        SysUser user = sysUserMapper.selectByAccount(account);
        System.out.println(user);
        // 判断用户是否存在
        if(user == null) {
    
    
            throw new UsernameNotFoundException("用户名不存在");
        }

        // todo 添加权限

        // 返回UserDetails实现类
        // 此User为 org.springframework.security.core.userdetails 包下的对象
        return new User(user.getAccount(), user.getPassword(), authorities);
    }
}

It has not been completed so far, and if it is run directly, an error will be reported:
java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"
Spring security does not allow the password to be in plain text, either indicate the encryption mode, or customize the password decoding method.
The custom password decoding method needs to be written in the Spring security configuration class:


@Configuration
//加载了WebSecurityConfiguration配置类, 配置安全认证策略。
//加载了AuthenticationConfiguration,
@EnableWebSecurity
//用来构建一个全局的AuthenticationManagerBuilder的标志注解
//开启基于方法的安全认证机制,也就是说在web层的controller启用注解机制的安全确认
@EnableGlobalMethodSecurity(prePostEnabled = true)
//Web Security 配置类
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    
    
	/**
     * 添加密码校验
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    
    
        auth.userDetailsService(sysUserDetailService).passwordEncoder(new PasswordEncoder() {
    
    
            @Override
            public String encode(CharSequence rawPassword) {
    
    
                System.out.println(rawPassword.toString());
                //todo 密码解密
                return rawPassword.toString();
            }

            @Override
            public boolean matches(CharSequence rawPassword, String encodedPassword) {
    
    
                System.out.println(rawPassword.toString());
                return encodedPassword.equals(rawPassword.toString());
            }
        });
    }
}

Part.3 Separate the front and back ends for verification

First, a login method is required

public class LoginServiceImpl {
    
    

    private final AuthenticationManager authenticationManager;

    /**
     *  登录获取token
     * @param sysUser
     * @return token
     */
    public String login(SysUser sysUser) {
    
    
        //封装校验对象
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(sysUser.getAccount(),sysUser.getPassword());
        //使用Manager调用 SysUserDetailService
        Authentication authentication = authenticationManager.authenticate(authenticationToken);
        //认证失败,内容为空
        if (Objects.isNull(authentication)){
    
    
            throw new RuntimeException("登录失败");
        }

        return JWTUtil.createToken(String.valueOf(sysUser.getId()),"");
    }
}

authenticationManager.authenticate(authenticationToken) can invoke the UsernamePasswordAuthenticationFilter filter, and then enter the implementation class of UserDetailsService for login verification.
In the login method, a token is generated and returned to the front end. How to put the token in the header of the http request and use security verification, you need to create a filter yourself.

/**
 * Token拦截器
 * 使用Spring提供的OncePerRequestFilter保证单次
 */
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    
    

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
    
    
        //获取token
        String token = request.getHeader(JWTUtil.TOKEN_HEADER);
        if (!StringUtils.hasText(token)){
    
    
            //没有token 放行 后续过滤器会进行校验
            filterChain.doFilter(request,response);
            return;
        }
        //解析token

        //获取用户信息

        //存入SecurityContextHolder 用于全局获取当前用户
        SysUser user = new SysUser();
        user.setAccount("zhangsan");
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user, null, null);
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);

        //放行
        filterChain.doFilter(request,response);
    }
}

Register the filter into the SecurityConfig configuration file.


    private final JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;	

	@Override
    protected void configure(HttpSecurity http) throws Exception {
    
    
        http
                //设置权限定义哪些URL需要被保护、哪些不需要被保护。HttpSecurity对象的方法
                .authorizeRequests()
                .antMatchers("/auth/login").permitAll()   //放行login
                //认证通过后任何请求都可访问。AbstractRequestMatcherRegistry的方法
                .anyRequest().authenticated()
                //连接HttpSecurity其他配置方法
                .and()
                //生成默认登录页,HttpSecurity对象的方法
                .formLogin()
                .permitAll()
                .and()
                .logout()
                .permitAll()
                .and()
                .csrf().disable()  //关闭跨站请求伪造
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) //不通过Session获取SecurityContext
                .and().cors()   //允许跨域
                //.and().exceptionHandling()
                //.authenticationEntryPoint(getAccessDeniedHandler()) //无权限默认返回设置
        ;

        //添加Token过滤器 放在UsernamePasswordAuthenticationFilter执行之前
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
    }

There is only one line to add a filter http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);. The above are some security configurations, mainly for .antMatchers("/auth/login").permitAll()the release request interface. Otherwise, token verification is also required to access the login interface, and an infinite loop is generated.

Part4. Log out


    /**
     * 登出系统
     */
    public Result logout() {
    
    
        //1、获取SecurityContextHolder中的对象
        UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
        SysUser sysUser = (SysUser) authentication.getPrincipal();
        sysUser.getId();
        //2、在存放用户token信息的位置清除用户信息,使其不能在JwtAuthenticationTokenFilter中认证成功,即为登出 一般redis
        return Result.success();
    }

Part5.1. Get the currently logged in user

A user object is injected into SecurityContextHolder in JwtAuthenticationTokenFilter , and in the actual method, just take it out.

Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
SysUser sysUser = (SysUser) authentication.getPrincipal();

Part5.2. Beautification authentication failed

Add the processor in SecurityConfig , which already exists in the configuration file of Part3.
.and().exceptionHandling() .authenticationEntryPoint(getAccessDeniedHandler()) //无权限默认返回设置
Specific filter writing method:

/**
 * 自定义Security校验失败返回类
 */
@Slf4j
public class AccessDeniedHandler implements AuthenticationEntryPoint {
    
    

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response,
                         AuthenticationException authException) throws IOException, ServletException {
    
    
        response.setCharacterEncoding(DEFAULT_CHARSET);
        response.setContentType(DEFAULT_CONTENT_TYPE);
        PrintWriter out = response.getWriter();

        out.write(JSON.toJSONString(Result.result(ResultCode.ErrTokenUnValid)));
        out.flush();
        out.close();
    }

}

Use **@Bean to inject into the getAccessDeniedHandler()** method.

Guess you like

Origin blog.csdn.net/qq_16253859/article/details/123536072