15.权限控制 + 置顶、加精、删除

目录

1.权限控制

1.1 登录检查

1.2 授权配置

1.3 认证方案

1.4 CSRF 配置

2.置顶、加精、删除

2.1 开发数据访问层

2.2 业务层

2.3 表现层


Spring Security 是一个专注于为 Java 应用程序提供身份认证和授权的框架,它的强大之处在于它可以轻松扩展以满足自定义需求。

特征:对身份的认证和授权提供全面的、课可扩展的支持;防止各种攻击,如会话固定攻击、点击劫持、csrf攻击等;支持与 Servlet API、Spring MVC 等 Web 技术集成


1.权限控制

  • 登录检查:之前采用拦截器实现了登录检查,这是简单的权限管理方案,现在将其废弃
  • 授权配置:对当前系统内包含的所有的请求,分配访问权限(普通用户、版主、管理员)
  • 认证方案:绕过 Security 认证流程,采用系统原来的认证方案
  • CSRF 配置:防止 CSRF 攻击的基本原理,以及表单、AJAX 相关的配置

1.1 登录检查

引入依赖:

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

废弃拦截器(WebMvcConfig 类中注掉 登录状态拦截器)

1.2 授权配置

首先在常量接口增加常量,在配置的时候需要指定哪些权限访问哪些用户

在 CommunityConstant 类中添加常量:

  • 普通用户权限、管理员权限、版主
    /**
     * 权限: 普通用户
     */
    String AUTHORITY_USER = "user";

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

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

在 config 包下新建 SecurityConfig 配置类:

  • 添加注解 @Configuration,并且继承 WebSecurityConfigurerAdapter,实现常量接口
  • 重写 configure(WebSecurity web) 方法:忽略对静态资源拦截(直接访问)
  • 重写 configure(HttpSecurity http) 方法:进行授权 和 权限不够的处理(当前项目中有多种请求(普通请求、异步请求),普通请求期望服务器返回 HTML,异步请求期望返回 JSON)
  • 权限不够的处理分为 没有登陆的处理和权限不足的处理(匿名实现接口)
  • 没有登陆的处理:判断同步异步请求——请求消息头某个值如果XMLHttpRequest 是异步请求,否则是同步请求
  • 如果是异步请求,给浏览器输出响应 JSON 字符串,手动处理(声明返回的类型),获取字符流,向前台输出内容,没有权限返回403
  • 同步请求:重定向到登陆页面 
  • 权限不足的处理:同上(重定向到没有权限的界面)
  • 处理没有权限路径(HomeController 类):
    //拒绝访问时的提示页面
    @RequestMapping(path = "/denied", method = RequestMethod.GET)
    public String getDeniedPage() {
        return "/error/404";
    }
  • Security底层默认会拦截 /logout 请求,进行退出处理;覆盖它默认的逻辑,才能执行我们自己的退出代码.
package com.example.demo.config;

import com.example.demo.util.CommunityConstant;
import com.example.demo.util.CommunityUtil;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter implements CommunityConstant {

    //重写 configure(WebSecurity web) 方法:忽略对静态资源拦截(直接访问)
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/resources/**");
    }

    //重写 configure(HttpSecurity http) 方法:进行授权 和 权限不够的处理
    @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();

        // 权限不够时的处理
        http.exceptionHandling()
                .authenticationEntryPoint(new AuthenticationEntryPoint() {
                    //没有登陆
                    @Override
                    public void commence(HttpServletRequest request, HttpServletResponse response,
                                         AuthenticationException authException) throws IOException, ServletException {
                        //判断同步异步请求——请求消息头某个值如果XMLHttpRequest 是异步请求,否则是同步请求
                        String xRequestedWith = request.getHeader("x-requested-with");
                        if ("XMLHttpRequest".equals(xRequestedWith)) {
                            //如果是异步请求,给浏览器输出响应 JSON 字符串,手动处理(声明返回的类型),获取字符流
                            //向前台输出内容,没有权限返回403
                            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 accessDeniedException) 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.3 认证方案

Security框架中,会把认证信息封装到token里,token会被一个filter获取到,并存入security context里。之后授权的时候,都是从security context中获取token,根据token判断权限

1️⃣查询某用户的权限(UserService)

  • 根据 UserId 查询 用户,通过 type 判断权限(结果存入集合中)
  • 实例化集合,添加集合中的数据,实现方法:判断——1是管理员,2是版主, 3是普通用户
    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;
    }

2️⃣LoginTicket 拦截器在请求一开始就会判断凭证,可以在此时对用户进行认证,并构建用户认证的结果,存入 SecurityContext ,以便于 Security 进行授权(LoginTicketInterceptor)

  • 创建认证结果,存储到接口 Authentication 中(实现类 UsernamePasswordAuthenticationToken,通常存入三个数据:用户、密码、权限)
  • 需要存储到 SecurityContext 中,而 SecurityContext 是通过 SecurityContextHolder 去处理
    //实现 preHandle(执行具体方法之前的预处理)方法:在请求开始获得 ticket,利用 ticket 查找对应的 user
    @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);
            //检查凭证是否有效:凭证不为空,并且状态是0,并且超时时间晚于当前时间才有效
            if (ticket != 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;
    }
  • 请求结束时也需要清理一下认证
     //最后还需要清理 hostHolder 中的 User(在整个请求结束之后),重写 afterCompletion 方法
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
                                Object handler, Exception ex) throws Exception {
        hostHolder.clear();

        //请求结束时也需要清理一下认证
        SecurityContextHolder.clearContext();
    }

3️⃣退出登录时也清理一下认证(LogicController 类)

    //退出业务方法
    @RequestMapping(path = "/logout", method = RequestMethod.GET)
    public String logout(@CookieValue("ticket") String ticket) {
        userService.logout(ticket);
        //退出登录时也清理一下认证
        SecurityContextHolder.clearContext();
        return "redirect:/login";//默认 GET 请求
    }

1.4 CSRF 配置

CSRF攻击原理:某网站盗取了你(浏览器)的cookie凭证,模拟你的身份访问服务器,通常利用 表单 提交数据。

防止CSRF攻击原理:Security会在每个表单中生成隐藏的 token,防止CSRF攻击

2.置顶、加精、删除

  • 功能实现:点击指定,修改帖子的类型;点击“加精”、“删除”,修改帖子的状态
  • 权限管理:版主可以执行“置顶”、“加精”操作;管理员可以执行“删除”操作
  • 按钮显示:版主可以看到“置顶”、“加精”按钮;管理员可以看到“删除”按钮

添加依赖:

<dependency>
	<groupId>org.thymeleaf.extras</groupId>
	<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>

2.1 开发数据访问层

对帖子操作,进行修改帖子:打开 dao 包下的 discussPostMapper.java 类:

  • 添加修改帖子类型、状态方法
    //修改帖子类型
    int updateType(int id, int type);
    
    //修改帖子状态
    int updateStatues(int id, int status);

打开配置文件(discusspost-mapper.xml)进行添加:

    <!--修改帖子类型-->
    <update id="updateType">
    update discuss_post set type = #{type} where id = #{id}
    </update>

    <!--修改帖子状态-->
    <update id="updateStatus">
        update discuss_post set status = #{status} where id = #{id}
    </update>

2.2 业务层

在 service 包下的 DiscussPostService 类进行添加:

  • 添加修改帖子类型、状态方法
    //修改帖子类型
    public int updateType(int id, int type) {
        return discussPostMapper.updateType(id, type);
    }

    //修改帖子状态
    public int updateStatus(int id, int status) {
        return discussPostMapper.updateStatus(id, status);
    }

2.3 表现层

在 controller 包下的 DiscussPostController 类下新添加置顶、加精、删除三个方法

  • 置顶:添加路径,并且置顶需要提交数据,是一个 POST 请求;而且还是一个异步请求,点击置顶按钮之后,不整体刷新,添加 @ResponseBody
  • 添加置顶方法,传入帖子 id:调用 discusssPostService 进行类型修改为1,此时帖子发生了变化,需要把最新的帖子数据进行同步到 elasticsearch 中,确保搜索到最新的帖子,需要触发帖子事件
  • 最终返回成功的提示
  • 加精、删除方法类似;只是删除不需要触发帖子事件,而是触发一个删帖事件,在CommunityConstant.java 中添加删帖主题
    /**
     * 主题: 删帖
     */
    String TOPIC_DELETE = "delete";
  • 删帖事件是新加事件,没有处理,需要在事件消费者中消费删贴事件(EventConsumer):类似于消费发帖事件
  •     // 消费删帖事件
        @KafkaListener(topics = {TOPIC_DELETE})
        public void handleDeleteMessage(ConsumerRecord record) {
            if (record == null || record.value() == null) {
                logger.error("消息的内容为空!");
                return;
            }
    
            Event event = JSONObject.parseObject(record.value().toString(), Event.class);
            if (event == null) {
                logger.error("消息格式错误!");
                return;
            }
    
            elasticsearchService.deleteDiscussPost(event.getEntityId());
        }
    //置顶
    //添加路径,并且置顶需要提交数据,是一个 POST 请求;而且还是一个异步请求,点击置顶按钮之后,不整体刷新,添加 @ResponseBody
    @RequestMapping(path = "/top", method = RequestMethod.POST)
    @ResponseBody
    public String setTop(int id) {
        //调用 discusssPostService 进行类型修改为1
        discussPostService.updateType(id, 1);

        //此时帖子发生了变化,需要把最新的帖子数据进行同步到 elasticsearch 中,确保搜索到最新的帖子,需要触发帖子事件
        // 触发发帖事件
        Event event = new Event()
                .setTopic(TOPIC_PUBLISH)
                .setUserId(hostHolder.getUser().getId())//当前用户
                .setEntityType(ENTITY_TYPE_POST)
                .setEntityId(id);
        eventProducer.fireEvent(event);

        return CommunityUtil.getJSONString(0);
    }

    //加精
    @RequestMapping(path = "/wonderful", method = RequestMethod.POST)
    @ResponseBody
    public String setWonderful(int id) {
        discussPostService.updateStatus(id, 1);

        // 触发发帖事件
        Event event = new Event()
                .setTopic(TOPIC_PUBLISH)
                .setUserId(hostHolder.getUser().getId())
                .setEntityType(ENTITY_TYPE_POST)
                .setEntityId(id);
        eventProducer.fireEvent(event);

        return CommunityUtil.getJSONString(0);
    }

    // 删除
    @RequestMapping(path = "/delete", method = RequestMethod.POST)
    @ResponseBody
    public String setDelete(int id) {
        discussPostService.updateStatus(id, 2);

        // 触发删帖事件
        Event event = new Event()
                .setTopic(TOPIC_DELETE)
                .setUserId(hostHolder.getUser().getId())
                .setEntityType(ENTITY_TYPE_POST)
                .setEntityId(id);
        eventProducer.fireEvent(event);

        return CommunityUtil.getJSONString(0);
    }

前端页面 discuss-detail.html:

                    <div class="float-right">
						<input type="hidden" id="postId" th:value="${post.id}">
						<button type="button" class="btn btn-danger btn-sm" id="topBtn"
								th:disabled="${post.type==1}" sec:authorize="hasAnyAuthority('moderator')">置顶</button>
						<button type="button" class="btn btn-danger btn-sm" id="wonderfulBtn"
								th:disabled="${post.status==1}" sec:authorize="hasAnyAuthority('moderator')">加精</button>
						<button type="button" class="btn btn-danger btn-sm" id="deleteBtn"
								th:disabled="${post.status==2}" sec:authorize="hasAnyAuthority('admin')">删除</button>
					</div>

discuss.js:

$(function(){
    $("#topBtn").click(setTop);
    $("#wonderfulBtn").click(setWonderful);
    $("#deleteBtn").click(setDelete);
});

function like(btn, entityType, entityId, entityUserId, postId) {
    $.post(
        CONTEXT_PATH + "/like",
        {"entityType":entityType,"entityId":entityId,"entityUserId":entityUserId,"postId":postId},
        function(data) {
            data = $.parseJSON(data);
            if(data.code == 0) {
                $(btn).children("i").text(data.likeCount);
                $(btn).children("b").text(data.likeStatus==1?'已赞':"赞");
            } else {
                alert(data.msg);
            }
        }
    );
}

// 置顶
function setTop() {
    $.post(
        CONTEXT_PATH + "/discuss/top",
        {"id":$("#postId").val()},
        function(data) {
            data = $.parseJSON(data);
            if(data.code == 0) {
                $("#topBtn").attr("disabled", "disabled");
            } else {
                alert(data.msg);
            }
        }
    );
}

// 加精
function setWonderful() {
    $.post(
        CONTEXT_PATH + "/discuss/wonderful",
        {"id":$("#postId").val()},
        function(data) {
            data = $.parseJSON(data);
            if(data.code == 0) {
                $("#wonderfulBtn").attr("disabled", "disabled");
            } else {
                alert(data.msg);
            }
        }
    );
}

// 删除
function setDelete() {
    $.post(
        CONTEXT_PATH + "/discuss/delete",
        {"id":$("#postId").val()},
        function(data) {
            data = $.parseJSON(data);
            if(data.code == 0) {
                location.href = CONTEXT_PATH + "/index";
            } else {
                alert(data.msg);
            }
        }
    );
}
  • 20
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

奋斗小温

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值