采用SpringSecurity进行用户的权限控制管理
- 之前采用拦截器实现了登录检查,这是简单的权限管理方案,现在将其废弃。
- 对当前系统内包含的所有的请求,分配访问权限 (普通用户、版主、管理员)
-绕过Security认证流程,采用系统原来的认证方案
注意:
关于认证:该项目未走security的认证方式,而是走自己实现的认证方式。
在security中进行认证时,是将认证信息封装进UsernamePasswordAuthenticationToken(如果认证信息是账号密码)中 ,而security底层的filter会将token存到securityContext中。
后面对用户进行权限判断时,要从securityContext中取出认证过的授权信息 。
这里我们需要绕过security底层认证逻辑,但是还要取到用户的权限信息,所以我们提供一个获取用户权限的方法,将用户权限在存入securityContex中 。
-防止 CSRF 攻击的基本原理,以及表单、AJAX相关的配置
csrf攻击是指某网站盗取用户的cookie中的凭证,冒充用户身份去访问服务器并向表单中提交数据。
发生在提交表单时,springsecurity解决方案:springsecurity在服务器向浏览器返回表单时,会返回一个隐藏的token(凭证),每次token都是随机的。
首先向该项目中增加一些常量,用来赋予用户权限(写在了常量接口CommunityConstant中)
/** * 权限:普通用户 */ String AUTHORITY_USER="user"; /** * 权限:管理员 */ String AUTHORITY_ADMIN="admin"; /** * 权限:版主 */ String AUTHORITY_MODERATOR="moderator";
import com.light.community.util.CommunityConstant;
import com.light.community.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;
/**
* @author light
* @Description springSecurity配置类
* @create 2023-05-31 15:24
*/
@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(
"/comment/add/**",
"/discuss/add",
"/follow",
"/unfollow",
"/like",
"/letter/**",
"/user/setting",
"/user/upload",
"/notice/**"
)
.hasAnyAuthority(
AUTHORITY_ADMIN,
AUTHORITY_MODERATOR,
AUTHORITY_USER
)
.anyRequest().permitAll(); //除了这些请求以外任何请求都是允许访问的
//.and().csrf().disable(); //不启用防止CSRF攻击
//权限不够时的处理
http.exceptionHandling() //不同请求,返回结果不同(eg:同步、异步,因此用处理器更合适一点
.authenticationEntryPoint(new AuthenticationEntryPoint() {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
//没有登录时处理
//考虑请求方式:如果普通请求,直接重定向到页面;如果异步请求,要返回json字符串
//通过请求头判断请求方式是同步或异步
String xRequestedWith = request.getHeader("x-requested-with");
if("XMLHttpRequest".equals(xRequestedWith)){
//异步请求
response.setContentType("application/plain;charset=utf-8"); //响应字符串:返回响应数据类型:普通字符串(plain
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 {
//登陆了但是权限不足时的处理
//考虑请求方式:如果普通请求,直接重定向到页面;如果异步请求,要返回json字符串
//通过请求头判断请求方式是同步或异步
String xRequestedWith = request.getHeader("x-requested-with");
if("XMLHttpRequest".equals(xRequestedWith)){
//异步请求
response.setContentType("application/plain;charset=utf-8"); //响应字符串:返回响应数据类型:普通字符串(plain
PrintWriter writer = response.getWriter();
writer.write(CommunityUtil.getJsonString(403,"你还没有访问此功能权限!"));
}else{
//同步请求
response.sendRedirect(request.getContextPath()+"/denied");
}
}
});
//security底层默认拦截/logout(退出)请求,进行退出处理
//因此要覆盖它的默认逻辑,才能执行自己的退出代码
http.logout()
.logoutUrl("/securitylogout");
/**
* 关于认证:该项目为走security的认证方式,而是自己实现的认证方式,
* 在security中进项认证,实现将认证信息封装进UsernamePasswordAuthenticationToken(如果认证信息是账号密码)中
* security底层的filter会将token存到securityContext中,后面进项权限判断时,要从securityContext中取出认证过的授权信息
* 因此我们要绕过security底层认证逻辑,但是还要取到用户的权限信息,所以我们提供一个获取用户权限的方法,将用户权限在存入securityContex中
*/
}
}
//获取用户权限
public Collection extends GrantedAuthority> getAuthority(int userId){
User user = this.findUserById(userId);
List list=new ArrayList<>(); //将用户权限信息存入list中
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;
}
@Override //在controller之前拦截
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//从cookie中获取ticket凭证
String ticket = CookieUtil.getValue(request, "ticket");
if(ticket!=null){
LoginTicket loginTicket = userService.findLoginTicket(ticket);
//查验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.getAuthority(user.getId())
);
SecurityContextHolder.setContext(new SecurityContextImpl(authentication));
}
}
return true;
}
防止CSRF攻击
当提交表单数据时,security会默认带上一个 token,但是当发送异步请求时,security不会提供隐藏的token,需要手动请求头中添加,这里仅演示index页面中的异步请求,即发帖功能的异步请求
牛客网-首页
$(function(){
$("#publishBtn").click(publish);
});
function publish() {
$("#publishModal").modal("hide");
//发送Ajax请求之前,提前将CSRF令牌设置到请求的消息头中
var token=$("meta[name='_csrf']").attr("content");
var header=$("meta[name='_csrf_header']").attr("content");
//在发送请求之前对页面进行设置 xhr:发送异步请求的核心对象
$(document).ajaxSend(function(e,xhr,options){
xhr.setRequestHeader(header,token); //设置请求头
});
//获取标题和内容
var title=$("#recipient-name").val();//id选择器
var content=$("#message-text").val();
//发送异步请求(POST)
$.post(
CONTEXT_PATH+"/discuss/add",
{"title":title,"content":content},
function(data){
data=$.parseJSON(data);
//在提示框中返回消息
$("#hintBody").text(data.msg);
//显示提示框
$("#hintModal").modal("show");
//两秒后自动隐藏
setTimeout(function(){
$("#hintModal").modal("hide");
//判断是否发送成功
if(data.code==0){
window.location.reload(); //重新加载页面
}
}, 2000);
}
);
}