Spring Security:认证+授权
认证:判断是否登录,账户是否存在,密码是否正确
授权:管理员和普通用户访问不同路径的权限不同,加以配置
登录检查:
普通的拦截器,通过实现HandlerInterceptor接口实现拦截器,通过实现WebMvcConfigurer接口实现一个配置类,在配置类中注入拦截器,最后再通过@Configuration注解注入配置。
这里废弃了HandlerInterceptor实现的拦截器,
WebMvcConfig.java
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private AlphaInterceptor alphaInterceptor;
@Autowired
private LoginTicketInterceptor loginTicketInterceptor;
@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(messageInterceptor)
.excludePathPatterns("/*/*.css", "/*/*.js", "/*/*.png", "/*/*.jpg", "/*/*.jpeg");
registry
.addInterceptor(dataInterceptor)
.excludePathPatterns("/*/*.css", "/*/*.js", "/*/*.png", "/*/*.jpg", "/*/*.jpeg");
}
}
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",
"/user/forgetPassword",
"/discuss/add",
"/discuss/my",
"/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/**", "/data/uv", "/data/dau", "/actuator/**")
.hasAnyAuthority(AUTHORITY_ADMIN)
.anyRequest()
.permitAll()
.and()
.csrf()
.disable();
// 权限不够时的处理
// 1.没有登录,这里有bug,登录后不刷新点赞,关注会显示没有登录
// 2.权限不足
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会被一个filter获取到,并存入security context里。之后授权的时候,都是从security context中获取token,根据token判断权限。
1、查询某用户的权限
UserService.java 查询用户的type
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";
}
1、CSRF攻击原理
某网站盗取了你(浏览器)的cookie凭证,模拟你的身份访问服务器,通常利用表单提交数据。
2、防止CSRF攻击原理
Security底层
Security会在每个表单中生成隐藏的token,防止CSRF攻击
3、异步请求时的处理
发帖异步请求,没有表单
A) 在 index.html 生成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);
}