项目1在线交流平台-7.构建安全高效的企业服务-2.使用Security自定义社区网页认证与授权


参考牛客网高级项目教程
尚硅谷SpringSecurity教程笔记

社区 Spring Security 从入门到进阶系列教程

功能需求

在这里插入图片描述

  • 1.有些网页请求需要登录者才有权限访问,采用security权限空值,废弃之前的登录检查拦截器控制
  • 2.对社区网页中的不同请求分配不同的访问权限
  • 3.登录用户密码验证认证,之前系统中认证逻辑已经很完善了,可以绕过Security默认认证流程,采用自定义的认证逻辑
  • 4.CSRF配置更新,原配置只能处理普通的表单请求,对于异步的AJAX表单请求,需要自定义CSRF配置

一、 废弃登录检查的拦截器

  • 拦截器配置中,将此拦截器注释掉即可,即不会将拦截器类添加到系统拦截器中去管理

在这里插入图片描述

二、授权配置

1. 导包

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

2. Security配置

  • 因使用自定义的认证逻辑,故,无需重写Security的认证方法AuthenticationManagerBuilder=
2.1 WebSecurity
  • 忽略静态资源的访问
@Override
public void configure(WebSecurity web) throws Exception {
    // 忽略静态资源的访问
    web
        .ignoring()
        .antMatchers("/resources/**");
}
2.2HttpSecurity
http.authorizeRequests()
  • 处理要验证权限的请求-即访问这些请求,需要有一定的权限
.antMatchers()-要验证的请求路径
.hasAnyAuthority( )-需要的权限
.anyRequest().permitAll()-请他请求无需权限访问
  • 注意,用户头像的请求,不能拦截,否则首页中普通访客看不到用户的头像
@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .antMatchers(   // 要验证权限的请求
                    "/user/setting",
                    "/user/upload",
                    "/discuss/add",
                    "/comment/add/**",
                    "/letter/**",
                    "/notice/**",
                    "/like",
                    "/follow",
                    "/unfollow"
            )
            .hasAnyAuthority( // 需要的权限
                    AUTHORITY_ADMIN,
                    AUTHORITY_MODERATOR,
                    AUTHORITY_USER
            )
            .anyRequest().permitAll();  // 请他请求无需权限访问
}
http.exceptionHandling()
  • 当访问某些请求,权限不足而出现异常后处理的逻辑
authenticationEntryPoint-认证没通过

参考博客

  • 他所建模的概念是:“认证入口点”。处理用户请求处理过程中遇到认证异常的接口

  • ExceptionTranslationFilter用于开启特定认证方案(authentication schema)的认证流程。

  • 该接口只定义了一个方法 :

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, 
           			AuthenticationException e) throws IOException, ServletException { 
    }
    
    • 这里参数request是遇到了认证异常authException用户请求,
    • response是将要返回给客户的相应,
    • 方法commence实现,也就是相应的认证方案逻辑会修改response并返回给用户引导用户进入认证流程。

处理逻辑参考之前统一处理异常的逻辑

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)) {  // 异步请求,向前端用JSON写入错误提示信息
                    response.setContentType("application/plain;charset=utf-8"); // 设置格式:普通字符串
                    PrintWriter writer = response.getWriter();
                    writer.write(CommunityUtil.getJSONString(403, "您还没有登录哦!"));
                } else {    // 普通请求-直接重定向到登录页面
                    response.sendRedirect(request.getContextPath() + "/login");
                }
            }
        })
.accessDeniedHandler-权限不够
  • 处理权限不够的异常接口,接口中也定义了一个处理的逻辑方法
  • 在方法中进行处理即可
    • 异步请求
    • 普通请求
.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");
        }
    }
});
  • 定义提示没有权限访问的网页请求

    @RequestMapping(path = "/denied", method = RequestMethod.GET)
    public String getDeniedPage() {
        return "/error/404";
    }
    
http.logout()-退出设置
  • 因为Security是过滤器,会在Controller之前拦截默认的/logout/请求,就不会走自定义的/logout/请求

  • 故,为了处理自定义的退出逻辑,在配置中,将默认的请求路径改了,就不会去拦截/logout/请求

    在这里插入图片描述

// 退出设置-不走security自定义的退出逻辑,走自定义的退出逻辑
http.logout()
        .logoutUrl("/securitylogout");// 覆盖默认设置的/logout路径,走自己定义的退出逻辑

三、使用自定义认证系统

1. 处理思路

  • 前面配置中,没有对认证方法进行重写处理,是因为项目中前期的认证逻辑已经很完善,可以直接使用
  • 为了使用自定义的认证逻辑,需要将自己认证逻辑与Security结合
  • Security认证处理后,都会将认证信息放入token容器里,每次认证都会从token中取出主体信息进行验证
  • 因此,可以将之前自定义的认证处理的结果也设法放进Security的token容器内,再将token放入SecurityContext上下文中
    • 就可以让Security走之前定义的认证处理逻辑
    • 因为Context上下文对所有请求都是共享的,这样逻辑上可以对所有请求进行认证

在这里插入图片描述

2. 在Service业务层增加查询权限的方法

  • 自定义的User实体类和Service类可以不用继承UserDetailUserDetailsService 接口,不需要Security管理这个主体
  • 但是,需要在自定义的业务层中,添加根据用户id查询指定用户权限的方法,这样才能构建认证信息交给token
/**
 * 查询指定用户的权限
 * @param userId    指定用户id
 * @return          权限集合-有可能一个用户有多个权限
 */
public Collection<? extends GrantedAuthority> getAuthorities(int userId) {
    User user = this.findUserById(userId);  // 用service包装的方法-从redis中查
    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;
}

3.认证信息进token和Context时机

  • 之前通过拦截器进行认证登录状态,判断是否登录,
    • 是给用户一个ticket,用户访问时,拿到这个ticket,去数据库中查找凭证
    • 根据凭证信息认证用户是否是登录状态,如果认证通过,
    • 将用户信息存入到当前请求线程容器中,方便每个请求获取使用
  • 同样逻辑,认证信息要再存一份放入token里,方便Security进行权限管理
new UsernamePasswordAuthenticationToken()-存入token
SecurityContextHolder.setContext()-存入context
// 在Controller之前执行
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    logger.debug("preHandle " + handler.toString());
    // 1.获取ticket
    String ticket = CookieUtil.getValue(request, "ticket");
    // 2.根据ticket查询user
    if(ticket != null) {
        // 向数据库中查询凭证
        LoginTicket loginTicket = userService.selectByTicket(ticket);
        if(loginTicket != null &&
            loginTicket.getStatus() == 0 &&
            loginTicket.getExpired().after(new Date())) {
            // 有效,就根据凭证找到用户信息
            User user = userService.findUserById(loginTicket.getUserId());
            
            // 在本次请求中持有用户信息,在请求结束前一直保存在请求的当前线程容器中
            hostHolder.setUsers(user);
            
            // 将认证信息-认证主体及主体的权限在存一份到Security的token中
            Authentication authentication = new UsernamePasswordAuthenticationToken
                    (user, user.getPassword(), userService.getAuthorities(user.getId()));
            // 再将认证信息存入SecurityContext中
            SecurityContextHolder.setContext(new SecurityContextImpl(authentication));
        }
    }
    return true;    // true,表示拦截处理之后继续执行
}

4. 请求之后和退出登录后要清理容器的认证信息

SecurityContextHolder.clearContext()-清理容器

请求之后清理

// 在模板视图渲染后处理,一般做清理工作
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    logger.debug("afterHandle " + handler.toString());
    hostHolder.clear();
    SecurityContextHolder.clearContext();
}

退出登录后清理

/**
 * 退出登录请求
 * @param ticket    登录凭证t票
 * @return
 */
@RequestMapping(path = "/logout", method = RequestMethod.GET)
public String logout(@CookieValue("ticket") String ticket) {
    userService.logout(ticket);
    SecurityContextHolder.clearContext();
    return "redirect:/login";
}

四、处理CSRF攻击配置

Web安全攻击XSS与CSRF攻击简述

  • 配置CFRS(盗取cooike中的凭证,模仿你的身份提交表单,盗取你的数据)

  • 普通请求SpringSecurity已经自定义配置好了,异步的需要自己处理

1. 防止CFRS攻击原理

  • CSRF俗称跨站协议伪造(跨站请求攻击)

  • 过程模拟

    img

    • 即A访问表单,在提交表单前,访问了B网站
    • B窃取了A的cookie,伪造成A的身份向服务器提交了表单form,窃取数据
  • 防止攻击策略

    在这里插入图片描述
    • 服务器向信任的浏览器发放一个随机的tocken放在提交的表单form中
    • 浏览器提交表单时,会连同tocken一起提交给服务器去验证
    • 其他网站就算盗用了ticket,但无法知道表单中的tocken,故,会被服务器拒绝访问

2. Security自动配置了普通请求的CSRF攻击

  • 点开帖子详情页面,里面有很多提交表单请求
  • 查看源代码,会发现隐藏的标签里传入了CSRF的tocken随机值

在这里插入图片描述

3. 异步请求自己配置csrf的tocken

HTML <meta> 标签
标签定义及使用说明
  • 元数据(Metadata)是数据的数据信息。
  • 标签提供了 HTML 文档的元数据。元数据不会显示在客户端,但是会被浏览器解析
提示和注释

注意:<meta> 标签通常位于<head>区域内

注意: 元数据通常以 名称/值 对出现。

注意: 如果没有提供 name 属性,那么名称/值对中的名称会采用 http-equiv 属性的值。

自定义<meta> 标签,传入csdf元数据
  • 访问该页面时,在此生成CSRF令牌,将Security中的元数据的传给页面,以key,value形式
<!--    访问该页面时,在此生成CSRF令牌,将Security中的元数据的传给页面,以key,value形式-->
   <!--csrf的key-->
   <meta name="_csrf" th:content="${_csrf.headerName}">
   <!--csrf的value-->
   <meta name="_csrf_header" th:content="${_csrf.token}">
在发布请求的js文件中设置-将csrf令牌发给浏览器携带
// 发送AJAX请求之前,将CSRF令牌设置到请求的消息头中.
// 使用JQuery选择器,取指定meta标签中的content属性的值,赋值给变量key,value
var header = $("meta[name='_csrf_header']").attr("content");
var token = $("meta[name='_csrf']").attr("content");
// 在发布异步请求前,先对请求进行设置
$(document).ajaxSend(function (e, xhr, options) {
    // xhr为发布异步请求的核心对象,设置请求头的key和value-设置完后可以放到浏览器中
    // 发送请求时,会携带这个数据
    xhr.setRequestHeader(header, token);
});

4. 关闭csrf

.and().csrf().disable()- 关闭csrf功能

5. 测试结果显示

  • 访问首页,mata标签携带数据

    在这里插入图片描述

  • 发布异步请求

    在这里插入图片描述

6. 其他异步请求配置

  • 同理,点赞、关注、取关、发送私信这些异步请求也要配置csrf,否则无法访问

点赞

  • 帖子详情页面
  • discuss.js

关注与取关

  • 个人主页页面
  • 关注和取关页面
  • profile.js

发送私信

  • 私信页面letter.html
  • 和私信详情页面
  • letter.js
  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值