- 登录检查
- 之前采用拦截器实现了登录检查,这是简单的权限管理方案,现在将其废弃
- 授权配置
- 对当前系统内包含的所有的请求,分配访问权限(普通用户、版主、管理员)
- 认证方式
- 绕过Security认证流程,采用原来的认证方案
- CSRF配置
- 防止CSRF攻击的基本原理,以及表单、AJAX相关的配置
1.导包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2.废弃登录拦截器
WebMvcConfig.java
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private AlphaInterceptor alphaInterceptor;
@Autowired
private LoginTicketInterceptor loginTicketInterceptor;
// @Autowired
// private LoginRequiredInterceptor loginRequiredInterceptor;
@Autowired
private MessageInterceptor messageInterceptor;
@Autowired
private DataInterceptor dataInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(alphaInterceptor)
.excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg")
.addPathPatterns("/register", "/login");
registry.addInterceptor(loginTicketInterceptor)
.excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");
// registry.addInterceptor(loginRequiredInterceptor)
// .excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");
registry.addInterceptor(messageInterceptor)
.excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");
registry.addInterceptor(dataInterceptor)
.excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");
}
}
Security配置类
1、在常量接口中定义权限,以便于使用
/**
* 权限: 普通用户
*/
String AUTHORITY_USER = "user";
/**
* 权限: 管理员
*/
String AUTHORITY_ADMIN = "admin";
/**
* 权限: 版主
*/
String AUTHORITY_MODERATOR = "moderator";
2、配置Security,主要是对授权的配置,因为认证使用原来的认证方案。所以没有登录认证的相关配置;此外,Security底层默认会拦截/logout请求,进行退出处理,为了执行我们自己的退出代码,要覆盖它默认的路径
@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(
"/user/setting",
"/user/upload",
"/discuss/add",
"/comment/add/**",
"/letter/**",
"/notice/**",
"/like",
"/follow",
"/unfollow"
)
.hasAnyAuthority( // 以上路径有以下任意权限都可以访问
AUTHORITY_USER,
AUTHORITY_ADMIN,
AUTHORITY_MODERATOR
)
.anyRequest().permitAll() //除了antMatchers里的路径之外,其它任何请求都允许
//.and().csrf().disable(); // 这是不启用防止CSRF
// 权限不够时的处理
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"); // 获取请求消息头的"x-requested-with"值
if ("XMLHttpRequest".equals(xRequestedWith)) { //如果该值是"XMLHttpRequest",说明是异步请求
response.setContentType("application/plain;charset=utf-8"); // 声明响应返回普通字符串(之后利用工具返回json字符串)
PrintWriter writer = response.getWriter(); //获取字符流
writer.write(CommunityUtil.getJSONString(403, "你还没有登录哦!"));// 响应应该是json字符串,通过工具去拼
} 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会被一个filter获取到,并存入security context里。之后授权的时候,都是从security context中获取token,根据token判断权限
1、查询某用户的权限
UerSevice.java
其实就是在根据用户userid查询type,type=1是管理员,2是版主, 3是普通用户。返回的是 GrantedAuthority类的集合
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.java
@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)); //SecurityContext是通过SecurityContextHolder处理
}
}
return true;
}
// 请求结束时,把保存的权限清理一下
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
hostHolder.clear();
SecurityContextHolder.clearContext();
}
3、退出登录时也清理一下认证
LoginController.java
@RequestMapping(path = "/logout", method = RequestMethod.GET)
public String logout(@CookieValue("ticket") String ticket) {
userService.logout(ticket);
SecurityContextHolder.clearContext();
return "redirect:/login";
}
CSRF
1、CSRF攻击原理:
某网站盗取了你(浏览器)的cookie凭证,模拟你的身份访问服务器,通常利用表单提交数据。
浅谈CSRF攻击方式
2、防止CSRF攻击原理
Security底层
Security会在每个表单中生成隐藏的token,防止CSRF攻击
3、异步请求时的处理
发帖异步请求,没有表单
A) 在 index.html 生成CSRF令牌
<!--访问该页面时,在此处生成CSRF令牌.-->
<meta name="_csrf" th:content="${_csrf.token}">
<meta name="_csrf_header" th:content="${_csrf.headerName}">
异步请求由于没有表单,所以通过请求消息头将数据传给服务器
B) 在 index.js 里通过请求消息头将数据传给服务器
$(function(){
$("#publishBtn").click(publish);
});
function publish() {
$("#publishModal").modal("hide");
// 发送AJAX请求之前,将CSRF令牌设置到请求的消息头中.
// 这样在提交数据时就会带有相应的token
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);
}
);
}
由于启用了防止CSRF攻击后,对每个异步请求都要进行这样的处理,否则服务器得不到该token会认为是被攻击,会禁止访问
附:获取json字符串工具
public static String getJSONString(int code, String msg, Map<String, Object> map) {
JSONObject json = new JSONObject();
json.put("code", code);
json.put("msg", msg);
if (map != null) {
for (String key : map.keySet()) {
json.put(key, map.get(key));
}
}
return json.toJSONString();
}
public static String getJSONString(int code, String msg) {
return getJSONString(code, msg, null);
}
public static String getJSONString(int code) {
return getJSONString(code, null, null);
}