问答平台(7),权限控制

登录检查

1
- 之前采用拦截器实现了登录检查,这是简单的权限管理方案,现在将其弃用。

引入依赖

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

弃用登录拦截器

  • WebMvcConfig: 修改
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    // @Autowired
    // private LoginRequiredInterceptor loginRequiredInterceptor;

    // 添加拦截器
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
    //将拦截器加入registry对象
    registry.addInterceptor(alphaInterceptor)
    .excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg")
    .addPathPatterns("/register", "/login");

    registry.addInterceptor(loginTicketInterceptor)
    .excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");


    // registry.addInterceptor(loginRequiredInterceptor)
    // .excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");

    registry.addInterceptor(messageInterceptor)
    .excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");

    registry.addInterceptor(dataInterceptor)
    .excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");
    }

授权配置

1
- 对当前系统内包含的所有请求,分配访问权限(普通用户、版主、管理员)。

常量接口

  • CommunityConstant: 增加内容
    1
    2
    3
    4
    5
    6
    7
    8
    // 权限:普通用户
    String AUTHORITY_USER = "user";

    // 权限:管理员
    String AUTHORITY_ADMIN = "admin";

    // 权限:版主
    String AUTHORITY_MODERATOR = "moderator";

config

  • SecurityConfig: 新增
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter implements CommunityConstant {

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

    @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_USER,
    AUTHORITY_ADMIN,
    AUTHORITY_MODERATOR
    )
    .anyRequest().permitAll();

    // 权限不够时的处理
    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");
    }
    }
    });

    // Security底层默认会拦截/logout请求,进行退出处理
    // 覆盖它默认的逻辑,才能执行我们自己的退出代码(善意的欺骗)
    http.logout().logoutUrl("/securitylogout");
    }

认证方案

1
- 绕过 Security 认证流程,采用系统原来的认证方案。

业务层

  • UserService: 修改
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    public Collection<? extends GrantedAuthority> getAuthorities(int userId) {
    User user = this.findUserById(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;
    }

拦截器

  • LoginTicketInterceptor: 修改
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    // 请求开始时通过ticket获取User信息,将信息存入ThreadLocal
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    // 从cookie中获取凭证
    String ticket = CookieUtil.getValue(request, "ticket");

    if (ticket != null) {
    // 查询凭证
    LoginTicket loginTicket = userService.findLoginTicket(ticket);
    // 检查凭证是否过期(有效)
    if (loginTicket != null && loginTicket.getStatus() == 0 && loginTicket.getExpired().after(new Date())) {
    // 根据凭证查询用户
    User user = userService.findUserById(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;
    }

    // 渲染后,清空ThreadLocal里面的User信息
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    hostHolder.clear();
    SecurityContextHolder.clearContext();
    }

业务层

  • LoginController: 修改
    1
    2
    3
    4
    5
    6
    @RequestMapping(path = "/logout", method = RequestMethod.GET)
    public String logout(@CookieValue("ticket") String ticket) {
    userService.logout(ticket);
    SecurityContextHolder.clearContext();
    return "redirect:/login";
    }

CSRF配置

1
2
- 防止CSRF攻击的基本原理,以及表单、AJAX相关的配置。
- CSRF: 跨站请求伪造

CSRF攻击-图示

页面

  • index.html: 修改
    1
    2
    3
    <!-- 访问该页面时,在此处生成CSRF令牌 -->
    <meta name="_csrf" th:content="${_csrf.token}">
    <meta name="_csrf_header" th:content="${_csrf.headerName}">
  • index.js: 修改
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function publish() {
    $("#publishModal").modal("hide");

    // 发送AJAX请求之前,将CSRF令牌设置到请求的消息头中
    var token = $("meta[name='_csrf']").attr("content");
    var header = $("meta[name='_csrf_header']").attr("content");
    $(document).ajaxSend(function (e, xhr, options) {
    xhr.setRequestHeader(header, token);
    });

    // 略
    }

禁用CSRF检查

1
2
每一个异步请求都需要csrf处理,为简化问题,禁用csrf:
http.authorizeRequests().and().csrf().disable()
  • SecurityConfig: 修改
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    @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_USER,
    AUTHORITY_ADMIN,
    AUTHORITY_MODERATOR
    )
    .anyRequest().permitAll()
    .and().csrf().disable();
    }

服务端报错

1
评论、点赞、关注(为空报错,修改代码)
  • MessageController: 修改(messageVO、model位置)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    @RequestMapping(path = "/notice/list", method = RequestMethod.GET)
    public String getNoticeList(Model model) {
    User user = hostHolder.getUser();

    // 查询评论类通知
    Message message = messageService.findLatestNotice(user.getId(), TOPIC_COMMENT);
    if (message != null) {
    Map<String, Object> messageVO = new HashMap<>();
    messageVO.put("message", message);

    // 转义字符(去掉)
    String content = HtmlUtils.htmlUnescape(message.getContent());
    Map<String, Object> data = JSONObject.parseObject(content, HashMap.class);

    messageVO.put("user", userService.findUserById((Integer) data.get("userId")));
    messageVO.put("entityType", data.get("entityType"));
    messageVO.put("entityId", data.get("entityId"));
    messageVO.put("postId", data.get("postId"));

    int count = messageService.findNoticeCount(user.getId(), TOPIC_COMMENT);
    messageVO.put("count", count);

    int unread = messageService.findNoticeUnreadCount(user.getId(), TOPIC_COMMENT);
    messageVO.put("unread", unread);

    model.addAttribute("commentNotice", messageVO);
    }

    // 查询点赞类通知
    message = messageService.findLatestNotice(user.getId(), TOPIC_LIKE);
    if (message != null) {
    Map<String, Object> messageVO = new HashMap<>();
    messageVO.put("message", message);

    String content = HtmlUtils.htmlUnescape(message.getContent());
    Map<String, Object> data = JSONObject.parseObject(content, HashMap.class);

    messageVO.put("user", userService.findUserById((Integer) data.get("userId")));
    messageVO.put("entityType", data.get("entityType"));
    messageVO.put("entityId", data.get("entityId"));
    messageVO.put("postId", data.get("postId"));

    int count = messageService.findNoticeCount(user.getId(), TOPIC_LIKE);
    messageVO.put("count", count);

    int unread = messageService.findNoticeUnreadCount(user.getId(), TOPIC_LIKE);
    messageVO.put("unread", unread);

    model.addAttribute("likeNotice", messageVO);
    }

    // 查询关注类通知
    message = messageService.findLatestNotice(user.getId(), TOPIC_FOLLOW);
    if (message != null) {
    Map<String, Object> messageVO = new HashMap<>();
    messageVO.put("message", message);

    String content = HtmlUtils.htmlUnescape(message.getContent());
    Map<String, Object> data = JSONObject.parseObject(content, HashMap.class);

    messageVO.put("user", userService.findUserById((Integer) data.get("userId")));
    messageVO.put("entityType", data.get("entityType"));
    messageVO.put("entityId", data.get("entityId"));

    int count = messageService.findNoticeCount(user.getId(), TOPIC_FOLLOW);
    messageVO.put("count", count);

    int unread = messageService.findNoticeUnreadCount(user.getId(), TOPIC_FOLLOW);
    messageVO.put("unread", unread);

    model.addAttribute("followNotice", messageVO);
    }

    // 查询未读消息数量
    int letterUnreadCount = messageService.findLetterUnreadCount(user.getId(), null);
    model.addAttribute("letterUnreadCount", letterUnreadCount);
    int noticeUnreadCount = messageService.findNoticeUnreadCount(user.getId(), null);
    model.addAttribute("noticeUnreadCount", noticeUnreadCount);

    return "/site/notice";
    }

页面

  • notice.html
    1
    2
    3
    4
    5
    6
    7
    8
    <!-- 内容 -->
    <!-- 通知列表 -->
    <!--评论类通知-->
    <li class="media pb-3 pt-3 mb-3 border-bottom position-relative" th:if="${commentNotice!=null}">
    <!--点赞类通知-->
    <li class="media pb-3 pt-3 mb-3 border-bottom position-relative" th:if="${likeNotice!=null}">
    <!--关注类通知-->
    <li class="media pb-3 pt-3 mb-3 border-bottom position-relative" th:if="${followNotice!=null}">

结果展示

服务端报错-图示


问答平台(7),权限控制
https://lcf163.github.io/2020/06/12/问答平台(7),权限控制/
作者
乘风的小站
发布于
2020年6月12日
许可协议