使用SpringSecurity认证与鉴权 | 社区项目

478 阅读5分钟

简介

一个SpringBoot的社区项目。

使用SpringSecurity完成登录认证和权限控制。

配置类

在5.7版本的SpringSecurity中,WebSecurityConfigurerAdapter已经Deprecated,我使用2.1.5.RELEASE版本的SpringSecurity,所以继续使用WebSecurityConfigurerAdapter类。

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter

Provides a convenient base class for creating a WebSecurityConfigurer instance. The implementation allows customization by overriding methods.

WebSecurityConfigurerAdapter (spring-security-docs 5.7.2 API)

In Spring Security 5.7.0-M2 we deprecated the WebSecurityConfigurerAdapter, as we encourage users to move towards a component-based security configuration.

Spring Security without the WebSecurityConfigurerAdapter

configure(WebSecurity web)

Modifier and TypeMethodDescription
voidconfigure(WebSecurity web)Override this method to configure WebSecurity.

忽略对静态资源的拦截

@Override
public void configure(WebSecurity web) throws Exception {
    web.ignoring().antMatchers("/resources/**");
}

configure(HttpSecurity http)

Modifier and TypeMethodDescription
protected voidconfigure(HttpSecurity http)Override this method to configure the HttpSecurity.

重写configure(HttpSecurity http)方法,我们主要的配置都是在这个方法中完成。

//授权
http.authorizeRequests()
        .antMatchers(
                "/user/setting",
                "/user/upload",
                "/discuss/add",
                "/comment/add/**",
                "/letter/**",
                "/notice/**",
                "/like",
                "/follow",
                "/unfollow"
        )
        .hasAnyAuthority(
                AUTHORITY_USER,
                AUTHORITY_ADMIN,
                AUTHORITY_MODERATOR
        )
        .antMatchers(
                "/discuss/top",
                "/discuss/wonderful"
        )
        .hasAnyAuthority(
                AUTHORITY_MODERATOR
        )
        .antMatchers(
                "/discuss/delete",
                "/data/**"
        )
        .hasAnyAuthority(
                AUTHORITY_ADMIN
        )
        .anyRequest().permitAll()//其他请求都允许
    	.and().csrf().disable();//不启用csrf

antMatchers中添加路径,hasAnyAuthority中添加可以访问对应路径的用户类型。

  • 普通用户AUTHORITY_USER、版主AUTHORITY_MODERATOR、管理员AUTHORITY_ADMIN可以进行用户设置/user/setting、发帖/discuss/add等操作

  • 版主AUTHORITY_MODERATOR可以置顶/discuss/top、加精/discuss/wonderful

  • 管理员AUTHORITY_ADMIN可以删帖/discuss/delete、查看网站数据/data/**


关于CSRF(跨站点请求伪造)

  1. 使用SpringSecurity后,生成页面时页面可从服务器获取到_csrf.token_csrf.headerName
  2. 发送AJAX请求之前,将CSRF令牌设置到请求的消息头中,_csrf.headerName作key,_csrf.token作value,效果就是请求头中多了一个字段X-CSRF-TOKEN: 1fef99fe-11cf-12e4-dg32-235236199a3f,字段值为举例。
  3. 服务器通过校验X-CSRF-TOKEN字段,判断本次请求是否可信。
  • 为系统中的每一个连接请求加上一个token,这个token是随机的,服务端对该token进行验证。破坏者在留言或者伪造嵌入页面的时候,无法预先判断CSRF token的值是什么,所以当服务端校验CSRF token的时候也就无法通过。所以这种方法在一定程度上是靠谱的。

SpringSecurity框架下实现CSRF跨站攻击防御_Tiger_Paul的博客-CSDN博客

由于CSRF的本质在于攻击者欺骗用户去访问自己设置的地址,所以如果要求在访问敏感数据请求时,要求用户浏览器提供不保存在cookie中,并且攻击者无法伪造的数据作为校验,那么攻击者就无法再运行CSRF攻击。这种数据通常是窗体中的一个数据项。服务器将其生成并附加在窗体中,其内容是一个伪随机数。当客户端通过窗体提交请求时,这个伪随机数也一并提交上去以供校验。正常的访问时,客户端浏览器能够正确得到并传回这个伪随机数,而通过CSRF传来的欺骗性攻击中,攻击者无从事先得知这个伪随机数的值,服务端就会因为校验token的值为空或者错误,拒绝这个可疑请求。

Spring Security系列(9)-CSRF详解_云烟成雨TD的博客-CSDN博客_csrf spring


  • 没有登录
    • 异步请求:返回json格式的提示
    • 普通请求:重定向到登录页面
  • 权限不足
    • 异步请求:返回json格式的提示
    • 普通请求:重定向到404页面
// 权限不够时的处理
http.exceptionHandling()
        .authenticationEntryPoint(new AuthenticationEntryPoint() {
            // 没有登录
            @Override
            public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
                String xRequestedWith = request.getHeader("x-requested-with");
                if ("XMLHttpRequest".equals(xRequestedWith)) {
                    //异步请求
                    response.setContentType("application/plain;charset=utf-8");
                    PrintWriter writer = response.getWriter();
                    writer.write(CommunityUtil.getJSONString(403, "你还没有登录哦!"));
                } else {
                    //普通请求
                    response.sendRedirect(request.getContextPath() + "/login");
                }
            }
        })
        .accessDeniedHandler(new AccessDeniedHandler() {
            // 权限不足
            @Override
            public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
                String xRequestedWith = request.getHeader("x-requested-with");
                if ("XMLHttpRequest".equals(xRequestedWith)) {
                    response.setContentType("application/plain;charset=utf-8");
                    PrintWriter writer = response.getWriter();
                    writer.write(CommunityUtil.getJSONString(403, "你没有访问此功能的权限!"));
                } else {
                    response.sendRedirect(request.getContextPath() + "/denied");
                }
            }
        });

异步请求的请求头有x-requested-with字段,且内容为XMLHttpRequest,通过请求头区分异步请求与普通请求。


Security底层默认会拦截/logout请求,进行退出处理。我们用/securitylogout覆盖它默认的逻辑,访问/logout时就会走我们自己写的逻辑。

http.logout().logoutUrl("/securitylogout");
@RequestMapping(path = "/logout", method = RequestMethod.GET)
public String logout(@CookieValue("ticket") String ticket) {
    userService.logout(ticket);
    SecurityContextHolder.clearContext();
    return "redirect:/login";
}

不使用SpringSecurity完成登录控制,使未登录的用户不能使用用户设置、发帖等功能。

  1. 自定义主注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginRequired {
}
  1. 给需要登录才能访问的功能添加@LoginRequired注解,如用户设置、发帖
  2. 写拦截器,用户访问的如果是有@LoginRequired注解的功能,必须登录后才能使用
@Component
public class LoginRequiredInterceptor implements HandlerInterceptor {

    @Autowired
    private HostHolder hostHolder;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            Method method = handlerMethod.getMethod();
            LoginRequired loginRequired = method.getAnnotation(LoginRequired.class);
            if (loginRequired != null && hostHolder.getUser() == null) {
                //有LoginRequired注解且没有登录
                response.sendRedirect(request.getContextPath() + "/login");//重定向到登录页面
                return false;
            }
        }

        return true;
    }
}

拦截器

  • preHandle方法中
    • 从cookie中获取ticket
    • 验证ticket
    • 根据ticket获取用户
    • 用户信息存入hostHolder
    • 用户信息存入SecurityContextHolder
  • afterCompletion方法中
    • hostHolder.clear();
    • SecurityContextHolder.clearContext();
@Component
public class LoginTicketInterceptor implements HandlerInterceptor {

    @Autowired
    private UserService userService;

    @Autowired
    private HostHolder hostHolder;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //从cookie中获取凭证
        String ticket = CookieUtil.getValue(request, "ticket");
        if (ticket == null) {
            return true;
        }

        LoginTicket loginTicket = userService.getLoginTicket(ticket);

        //查询凭证是否有效
        if (loginTicket == null
                || loginTicket.getStatus() != 0
                || !loginTicket.getExpired().after(new Date())) {
            return true;
        }

        User user = userService.getUser(loginTicket.getUserId());

        //在本次请求中持有用户
        hostHolder.setUser(user);

        //构建用户认证的结果,并存入securityContext,以便以security进行授权
        Authentication authentication = new UsernamePasswordAuthenticationToken(
                user, user.getPassword(), userService.getAuthorities(user.getId())
        );
        SecurityContextHolder.setContext(new SecurityContextImpl(authentication));


        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        if (modelAndView != null && hostHolder.getUser() != null) {
            modelAndView.addObject("loginUser", hostHolder.getUser());
        }
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        hostHolder.clear();

        SecurityContextHolder.clearContext();
    }
}

Associates a given SecurityContext with the current execution thread.

Modifier and TypeMethod and Description
static voidclearContext()Explicitly clears the context value from the current thread.
static voidsetContext(SecurityContext context)Associates a new SecurityContext with the current thread of execution.

SecurityContextHolder (Spring Security 4.2.15.RELEASE API)


UserServiceImpl中根据用户id获取对应的权限的方法。

public Collection<? extends GrantedAuthority> getAuthorities(int userId) {
    User user = this.getUser(userId);

    List<GrantedAuthority> list = new ArrayList<>();
    list.add(new GrantedAuthority() {

        @Override
        public String getAuthority() {
            switch (user.getType()) {
                case 1:
                    return AUTHORITY_ADMIN;
                case 2:
                    return AUTHORITY_MODERATOR;
                default:
                    return AUTHORITY_USER;
            }
        }
    });
    return list;
}

JWT

以后使用JWT改进登录

一文带你搞懂 JWT 常见概念 & 优缺点 - 掘金 (juejin.cn)

厉害!我带的实习生仅用四步就整合好SpringSecurity+JWT实现登录认证! - 掘金 (juejin.cn)