15.权限控制 + 置顶、加精、删除
目录
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);
}
}
);
}
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!