前后端分离的Spring Security详解

1,238 阅读2分钟

这篇博文主要分析了下面这个博客中的例子: www.yangbuyi.top/archives/%E…
上面这个博客中的例子:

SecurityContextHolder.getContext()获取安全上下文对象, 就是那个保存在 ThreadLocal 里面的安全上下文对象

jwt解决用户认证问题,有没有带token访问
spring-security解决用户是否有权限,角色访问特性url

首先看一下Security配置

@Override
protected void configure(HttpSecurity http) throws Exception {
    // 自定义登录校验过滤器一定要在UsernamePasswordAuthenticationFilter 执行之前 执行
    http.addFilterBefore(jwtCheckFilter, UsernamePasswordAuthenticationFilter.class);
    // 关闭csrf攻击
    http.csrf().disable();
    // 登录配置
    http.formLogin()
            .loginProcessingUrl("/doLogin") // 指定登录地址
            .successHandler(authenticationSuccessHandler()) // 登录成功执行的
            .failureHandler(authenticationFailureHandler()) // 登录失败执行的
            .permitAll();
    // 403 权限不足
    http.exceptionHandling().accessDeniedHandler(accessDecision);
    // 基于token方式 不会存储session来进行登录判断了
    http.sessionManagement().disable();
    // 集成JWT 全部放行接口  使用jwt过滤器来进行 鉴权
    http.authorizeRequests().antMatchers("/**").permitAll();

}

可以看到,配置里所有接口都是放行的,jwtCheckFilter的作用是验证token/doLogin是不需要认证token的。

登录流程:

1,/doLogin,首先会执行jwtCheckFilter,下面的代码可以看到jwtCheckFilter过滤器是直接放行/doLogin接口的。

if ("/doLogin".equals(requestURI) && "POST".equalsIgnoreCase(method)) {
    filterChain.doFilter(httpServletRequest, httpServletResponse);
    return;
}

2,再会调用 UserDetailsServiceImpl 类中的 loadUserByUsername 方法,该方法会根据用户名(/doLogin接口携带的username参数),获取到用户的详细信息(一般从数据库中获取),该详细信息包括用户密码,用户所拥有的权限(从用户权限表中查询)。loadUserByUsername方法返回 UserDetails 对象,该类是Spring Security自带的类。为什么会走到这个方法中,这个也是在Security配置文件中配置的:

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}

3,Spring Security自带框架会验证/doLogin接口携带的用户名和密码是否正确。
4,如果不正确,则会执行配置文件中配置的登录失败回调 AuthenticationFailureHandler,返回给前端登录失败的信息

@Bean
public AuthenticationFailureHandler authenticationFailureHandler() {
    return (request, response, exception) -> {
        response.setContentType("application/json;charset=utf-8");
        // 写出去
        HashMap<String, Object> map = new HashMap<>(4);
        map.put("code", 401);
        map.put("msg", "登陆失败");
        ObjectMapper objectMapper = new ObjectMapper();
        String s = objectMapper.writeValueAsString(map);
        PrintWriter writer = response.getWriter();
        writer.write(s);
        writer.flush();
        writer.close();
    };
}

5,正确,则会执行配置文件中配置的登录成功回调 AuthenticationSuccessHandler,在这个成功回调中,使用jwt类库生成jwt token返回给前端,随后需要携带token的接口,前端都需要携带这个token

UserDetails principal = (UserDetails) authentication.getPrincipal();

上面的代码可以看到,在第2步中返回的UserDetails对象在用户名密码验证成功了,会存入到Authentication,将Authentication存入到SecurityContext,这样就可以在当前线程中类似于ThreadLocal一样获取到用户信息UserDetails

登录后的接口请求流程:

1,登录成功后的其他接口都需要携带token参数,这个是在jwtCheckFilter过滤器中验证的。Token在这个项目中是存到redis中的。
2,验证通过后,将用户信息包括其拥有的权限存到SecurityContextHolder上下文中,

UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, null, simpleGrantedAuthorities);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);

3,上述的token验证通过后,Spring Security框架则会通过第2步中得到的用户及其拥有的权限去判断该用户是否拥有访问当前url的权限,如果没有权限则会走到:

@Component
public class AccessDecision implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
        httpServletResponse.setContentType("application/json;charset=UTF-8");
        //按照系统自定义结构返回授权失败
        HashMap<String, Object> stringObjectHashMap = new HashMap<>(4);
        stringObjectHashMap.put("code", 403);
        stringObjectHashMap.put("msg", "权限不足");
        ObjectMapper objectMapper = new ObjectMapper();
        String s = objectMapper.writeValueAsString(stringObjectHashMap);
        PrintWriter writer = httpServletResponse.getWriter();
        writer.write(s);
        writer.flush();
        writer.close();
    }
}

中,AccessDeniedHandler也是在Security配置文件中配置的。返回给前端权限不足的提示。 4,如果用户拥有该权限,则会走到对应的controller方法中。

注意:匿名用户就是没有登录的用户,匿名用户当然就没有角色和权限,则无权访问使用@PreAuthorize注解的controller接口。当然上面这个博客例子并没有使用到authenticationEntryPoint,而是把匿名用户的验证都交给了jwtCheckFilter

//AuthenticationEntryPoint 用来解决匿名用户访问无权限资源时的异常
//AccessDeineHandler 用来解决认证过的用户访问无权限资源时的异常
.authenticationEntryPoint(authenticationErrorHandler)
.accessDeniedHandler(jwtAccessDeniedHandler)

参考:

1,www.cnblogs.com/RudeCrab/p/… 这个博客写的非常好
2,blog.csdn.net/xiaokanfuch…
3, blog.csdn.net/z69183787/a… 登录名密码什么时候验证,源码分析