目录
Spring Security 是一个专注于为 Java 应用程序提供身份认证和授权的框架,它的强大之处在于它可以轻松扩展以满足自定义需求。
特征:对身份的认证和授权提供全面的、课可扩展的支持;防止各种攻击,如会话固定攻击、点击劫持、csrf攻击等;支持与 Servlet API、Spring MVC 等 Web 技术集成
引入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
废弃拦截器(WebMvcConfig 类中注掉 登录状态拦截器)
首先在常量接口增加常量,在配置的时候需要指定哪些权限访问哪些用户
在 CommunityConstant 类中添加常量:
/**
* 权限: 普通用户
*/
String AUTHORITY_USER = "user";
/**
* 权限: 管理员
*/
String AUTHORITY_ADMIN = "admin";
/**
* 权限: 版主
*/
String AUTHORITY_MODERATOR = "moderator";
在 config?包下新建 SecurityConfig 配置类:
//拒绝访问时的提示页面
@RequestMapping(path = "/denied", method = RequestMethod.GET)
public String getDeniedPage() {
return "/error/404";
}
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");
}
}
Security框架中,会把认证信息封装到token里,token会被一个filter获取到,并存入security context里。之后授权的时候,都是从security context中获取token,根据token判断权限
1??查询某用户的权限(UserService)
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)
//实现 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 请求
}
CSRF攻击原理:某网站盗取了你(浏览器)的cookie凭证,模拟你的身份访问服务器,通常利用?表单 提交数据。
防止CSRF攻击原理:Security会在每个表单中生成隐藏的 token,防止CSRF攻击
添加依赖:
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
对帖子操作,进行修改帖子:打开 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>
在 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);
}
在 controller 包下的 DiscussPostController 类下新添加置顶、加精、删除三个方法
/**
* 主题: 删帖
*/
String TOPIC_DELETE = "delete";
// 消费删帖事件
@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);
}
}
);
}