BBS论坛项目相关-14:Spring Security与用户权限控制
需求:
登录检查:之前使用拦截器实现,现在改为SpringSecurity实现
授权配置:对当前系统内所包含的所有的请求,分配访问权限(普通用户,版主,管理员)
认证方案:绕过security认证流程,采用原本的认证方案
CSRF配置:防止CSRF攻击的基本原理,以及表单、AJAX相关的配置。
授权配置
在常量接口中配置用户权限常量。
public interface CommunityConstant {
/**
* 激活成功
*/
int ACTIVATION_SUCCESS = 0;
/**
* 重复激活
*/
int ACTIVATION_REPEAT = 1;
/**
* 激活失败
*/
int ACTIVATION_FAILURE = 2;
/**
* 默认状态的登录凭证的超时时间
*/
int DEFAULT_EXPIRED_SECONDS = 3600 * 12;
/**
* 记住状态的登录凭证超时时间
*/
int REMEMBER_EXPIRED_SECONDS = 3600 * 24 * 100;
/**
* 实体类型: 帖子
*/
int ENTITY_TYPE_POST = 1;
/**
* 实体类型: 评论
*/
int ENTITY_TYPE_COMMENT = 2;
/**
* 实体类型: 用户
*/
int ENTITY_TYPE_USER = 3;
/**
* 主题: 评论
*/
String TOPIC_COMMENT = "comment";
/**
* 主题: 点赞
*/
String TOPIC_LIKE = "like";
/**
* 主题: 关注
*/
String TOPIC_FOLLOW = "follow";
/**
* 主题: 发帖
*/
String TOPIC_PUBLISH = "publish";
/**
* 主题: 删帖
*/
String TOPIC_DELETE = "delete";
/**
* 主题: 分享
*/
String TOPIC_SHARE = "share";
/**
* 系统用户ID
*/
int SYSTEM_USER_ID = 1;
/**
* 权限: 普通用户
*/
String AUTHORITY_USER = "user";
/**
* 权限: 管理员
*/
String AUTHORITY_ADMIN = "admin";
/**
* 权限: 版主
*/
String AUTHORITY_MODERATOR = "moderator";
}
配置Security
忽略对静态资源的拦截
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/resources/**");
}
授权
配置需要拦截的请求路径,用户设置,评论,点赞等各个功能的请求都需要被拦截。只要有任何一个用户,管理员,版主权限就能访问。
配置权限不够时的操作,由于请求可能是普通请求,也可能是异步请求,所以需要做更细致的处理,不能直接返回页面。
采用authenticationEntryPoint对没有登陆时进行处理:根据请求头判断请求是异步请求还是普通请求,异步请求返回json字符串,普通请求则重定向
用accessDeniedHandler对权限不足时进行处理:与没有登陆时处理相似
注意: 默认拦截logout请求,会自行处理logout请求,到达默认的退出页面。想要显示我们自己的退出页面,需要覆盖退出页面。
@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
)
.antMatchers(
"/discuss/top",
"/discuss/wonderful"
)
.hasAnyAuthority(
AUTHORITY_MODERATOR
)
.antMatchers(
"/discuss/delete",
"/data/**",
"/actuator/**"
)
.hasAnyAuthority(
AUTHORITY_ADMIN
)
.anyRequest().permitAll()
.and().csrf().disable();
// 权限不够时的处理
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");
}
Security框架会把认证信息封装到一个token里,token会被Security的一个filter拦截到,然后filter会把token存在SecurityContext中,后面进行授权判断时,会从SecurityContext取得这个token进行授权判断。所以想使用我们自己的认证,需要把结果存在SecurityContext里。
需要在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;
}
之前使用拦截器实现登录验证,进行了两次拦截,一次是对除了静态资源的所有请求拦截判断cookie中是否有成功登陆后才存储的ticket。另一次是通过自定义注解对一个controller方法进行拦截,用于检查登陆状态。
现在通过SpringSecurity实现登陆状态检查,不再需要自定义注解。但还是使用原本的认证逻辑,只需要将认证结果封装到UsernamePasswordAuthenticationToken存在SecurityContextHolder中即可。
@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;
}
退出时清理SecurityContext
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
hostHolder.clear();
SecurityContextHolder.clearContext();
}
预防CSRF攻击
CSRF攻击:某网站盗取cookie中的登陆凭证,模拟用户身份访问服务器,利用表单向服务器提交数据来谋取利益。一般在提交表单时发生。
Security防止CSRF攻击,Security会在表单中生成一个隐藏的tocken,表单中就有一个随机tocken,每次请求都不一样。<input name="${(_csrf.parameterName)!}" value="${(_csrf.token)!}" type="hidden">
在前端页面处理防止CSRF攻击
对于异步请求,通过请求的消息头携带tocken
<!--访问该页面时,在此处生成CSRF令牌.-->
<meta name="_csrf" th:content="${_csrf.token}">
<meta name="_csrf_header" th:content="${_csrf.headerName}">
在发送异步请求之前,将tocken设置到请求的消息头中
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);
});
// 获取标题和内容
var title = $("#recipient-name").val();
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");
// 2秒后,自动隐藏提示框
setTimeout(function(){
$("#hintModal").modal("hide");
// 刷新页面
if(data.code == 0) {
window.location.reload();
}
}, 2000);
}
);
}
对所有异步请求页面都要进行处理,否则收不到tocken会认为是受到攻击不能访问