Spring Security + token前后端分离该怎么认证

前言

因为这个Spring Security学习的过程比较曲折,最初以为比较简单,但是实际上也确实比较简单,最大的坑点在于,大多数找到的关于Spring Security都不是基于前后端分离进行的配置,解决了一个bug发现了更多的bug,烦不胜烦,直到在著名的学习网站B站看到了这个视频,老师讲的真的简单,比之前看过的尚**、黑*的视频好很多(基于前后端分离),那两个机构的视频看的简直无语,一个安全框架中恨不得给你把Spring boot、mysql、Spring cloud都讲一遍,看得着实费解。如果你需要快速地投入到项目中,这都总结好了,直接用就是了!(版本:boot-2.6.3)

这里的token使用的是使用JWT生成的,这方面的文章比较多,就不赘述了。

Part.1 引入Security

因为是Spring亲儿子的关系,引入十分方便,直接在pom.xml中加入依赖即可

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

Part.2 登录校验接入数据库

Spring security中存在众多过滤器(Filter),登录模块会调用UsernamePasswordAuthentiocationFilter过滤器(重要!)去处理登录请求,只需要写一个类继承UserDetailsService并且实现loadUserByUsername方法就可以进行登陆校验,如果查询到用户并且返回UserDetails即为通过校验。

@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);
    }
}

到此还没有完成,如果直接运行会报错:
java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"
Spring security不允许密码是明文,要么标明加密模式,要么自定义密码解码方法。
自定义密码解码方法需要写在Spring security配置类中:


@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 前后端分离进行校验

首先,需要一个登陆方法

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)可以调起UsernamePasswordAuthentiocationFilter过滤器,随后进入UserDetailsService的实现类中进行登录校验。
登录方法中生成了一个token返回给了前端,如何将token放在http请求的header中并且使用security校验,需要自己建立一个过滤器。

/**
 * 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);
    }
}

将过滤器注册到SecurityConfig配置文件中。


    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);
    }

添加过滤器只有一行http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);,上面的是一些关于Security的配置,主要在于.antMatchers("/auth/login").permitAll()放行请求接口,否则访问登录接口也需要进行token校验,那就生成了一个死循环。

Part4.登出


    /**
     * 登出系统
     */
    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.获取当前登录用户

JwtAuthenticationTokenFilter中往SecurityContextHolder注入了一个用户对象,在实际的方法中,从中取出即可。

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

Part5.2.美化认证失败

SecurityConfig中加入处理器,在Part3的配置文件中已存在。
.and().exceptionHandling() .authenticationEntryPoint(getAccessDeniedHandler()) //无权限默认返回设置
具体的过滤器写法:

/**
 * 自定义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();
    }

}

使用**@Bean注入getAccessDeniedHandler()**方法中。

猜你喜欢

转载自blog.csdn.net/qq_16253859/article/details/123536072