参考牛客网高级项目教程
尚硅谷SpringSecurity教程笔记
功能需求
- 1.有些网页请求需要登录者才有权限访问,采用security权限空值,废弃之前的登录检查拦截器控制
- 2.对社区网页中的不同请求分配不同的访问权限
- 3.登录用户密码验证认证,之前系统中认证逻辑已经很完善了,可以绕过Security默认认证流程,采用自定义的认证逻辑
- 4.CSRF配置更新,原配置只能处理普通的表单请求,对于异步的AJAX表单请求,需要自定义CSRF配置
一、 废弃登录检查的拦截器
- 拦截器配置中,将此拦截器注释掉即可,即不会将拦截器类添加到系统拦截器中去管理
二、授权配置
1. 导包
<!-- 整合security-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2. Security配置
- 因使用自定义的认证逻辑,故,无需重写Security的认证方法
AuthenticationManagerBuilder=
2.1 WebSecurity
- 忽略静态资源的访问
@Override
public void configure(WebSecurity web) throws Exception {
// 忽略静态资源的访问
web
.ignoring()
.antMatchers("/resources/**");
}
2.2HttpSecurity
http.authorizeRequests()
- 处理要验证权限的请求-即访问这些请求,需要有一定的权限
.antMatchers()
-要验证的请求路径
.hasAnyAuthority( )
-需要的权限
.anyRequest().permitAll()
-请他请求无需权限访问
- 注意,用户头像的请求,不能拦截,否则首页中普通访客看不到用户的头像
@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_ADMIN,
AUTHORITY_MODERATOR,
AUTHORITY_USER
)
.anyRequest().permitAll(); // 请他请求无需权限访问
}
http.exceptionHandling()
- 当访问某些请求,权限不足而出现异常后处理的逻辑
authenticationEntryPoint
-认证没通过
-
他所建模的概念是:“认证入口点”。处理用户请求处理过程中遇到认证异常的接口,
-
被
ExceptionTranslationFilter
用于开启特定认证方案(authentication schema
)的认证流程。 -
该接口只定义了一个方法 :
@Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException { }
- 这里参数
request
是遇到了认证异常authException
用户请求, response
是将要返回给客户的相应,- 方法
commence
实现,也就是相应的认证方案逻辑会修改response
并返回给用户引导用户进入认证流程。
- 这里参数
处理逻辑:参考之前统一处理异常的逻辑
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)) { // 异步请求,向前端用JSON写入错误提示信息
response.setContentType("application/plain;charset=utf-8"); // 设置格式:普通字符串
PrintWriter writer = response.getWriter();
writer.write(CommunityUtil.getJSONString(403, "您还没有登录哦!"));
} else { // 普通请求-直接重定向到登录页面
response.sendRedirect(request.getContextPath() + "/login");
}
}
})
.accessDeniedHandler
-权限不够
- 处理权限不够的异常接口,接口中也定义了一个处理的逻辑方法
- 在方法中进行处理即可
- 异步请求
- 普通请求
.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");
}
}
});
-
定义提示没有权限访问的网页请求
@RequestMapping(path = "/denied", method = RequestMethod.GET) public String getDeniedPage() { return "/error/404"; }
http.logout()
-退出设置
-
因为Security是过滤器,会在Controller之前拦截默认的
/logout/
请求,就不会走自定义的/logout/
请求 -
故,为了处理自定义的退出逻辑,在配置中,将默认的请求路径改了,就不会去拦截
/logout/
请求
// 退出设置-不走security自定义的退出逻辑,走自定义的退出逻辑
http.logout()
.logoutUrl("/securitylogout");// 覆盖默认设置的/logout路径,走自己定义的退出逻辑
三、使用自定义认证系统
1. 处理思路
- 前面配置中,没有对认证方法进行重写处理,是因为项目中前期的认证逻辑已经很完善,可以直接使用
- 为了使用自定义的认证逻辑,需要将自己认证逻辑与Security结合
- Security认证处理后,都会将认证信息放入token容器里,每次认证都会从token中取出主体信息进行验证
- 因此,可以将之前自定义的认证处理的结果也设法放进Security的token容器内,再将token放入SecurityContext上下文中
- 就可以让Security走之前定义的认证处理逻辑
- 因为Context上下文对所有请求都是共享的,这样逻辑上可以对所有请求进行认证
2. 在Service业务层增加查询权限的方法
- 自定义的User实体类和Service类可以不用继承
UserDetail
、UserDetailsService
接口,不需要Security管理这个主体 - 但是,需要在自定义的业务层中,添加根据用户id查询指定用户权限的方法,这样才能构建认证信息交给token
/**
* 查询指定用户的权限
* @param userId 指定用户id
* @return 权限集合-有可能一个用户有多个权限
*/
public Collection<? extends GrantedAuthority> getAuthorities(int userId) {
User user = this.findUserById(userId); // 用service包装的方法-从redis中查
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;
}
3.认证信息进token和Context时机
- 之前通过拦截器进行认证登录状态,判断是否登录,
- 是给用户一个ticket,用户访问时,拿到这个ticket,去数据库中查找凭证
- 根据凭证信息认证用户是否是登录状态,如果认证通过,
- 将用户信息存入到当前请求线程容器中,方便每个请求获取使用
- 同样逻辑,认证信息要再存一份放入token里,方便Security进行权限管理
new UsernamePasswordAuthenticationToken()
-存入token
SecurityContextHolder.setContext()
-存入context
// 在Controller之前执行
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
logger.debug("preHandle " + handler.toString());
// 1.获取ticket
String ticket = CookieUtil.getValue(request, "ticket");
// 2.根据ticket查询user
if(ticket != null) {
// 向数据库中查询凭证
LoginTicket loginTicket = userService.selectByTicket(ticket);
if(loginTicket != null &&
loginTicket.getStatus() == 0 &&
loginTicket.getExpired().after(new Date())) {
// 有效,就根据凭证找到用户信息
User user = userService.findUserById(loginTicket.getUserId());
// 在本次请求中持有用户信息,在请求结束前一直保存在请求的当前线程容器中
hostHolder.setUsers(user);
// 将认证信息-认证主体及主体的权限在存一份到Security的token中
Authentication authentication = new UsernamePasswordAuthenticationToken
(user, user.getPassword(), userService.getAuthorities(user.getId()));
// 再将认证信息存入SecurityContext中
SecurityContextHolder.setContext(new SecurityContextImpl(authentication));
}
}
return true; // true,表示拦截处理之后继续执行
}
4. 请求之后和退出登录后要清理容器的认证信息
SecurityContextHolder.clearContext()
-清理容器
请求之后清理
// 在模板视图渲染后处理,一般做清理工作
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
logger.debug("afterHandle " + handler.toString());
hostHolder.clear();
SecurityContextHolder.clearContext();
}
退出登录后清理
/**
* 退出登录请求
* @param ticket 登录凭证t票
* @return
*/
@RequestMapping(path = "/logout", method = RequestMethod.GET)
public String logout(@CookieValue("ticket") String ticket) {
userService.logout(ticket);
SecurityContextHolder.clearContext();
return "redirect:/login";
}
四、处理CSRF攻击配置
-
配置CFRS(盗取cooike中的凭证,模仿你的身份提交表单,盗取你的数据)
-
普通请求SpringSecurity已经自定义配置好了,异步的需要自己处理
1. 防止CFRS攻击原理
-
CSRF俗称跨站协议伪造(跨站请求攻击)
-
过程模拟:
- 即A访问表单,在提交表单前,访问了B网站
- B窃取了A的cookie,伪造成A的身份向服务器提交了表单form,窃取数据
-
防止攻击策略
- 服务器向信任的浏览器发放一个随机的tocken放在提交的表单form中
- 浏览器提交表单时,会连同tocken一起提交给服务器去验证
- 其他网站就算盗用了ticket,但无法知道表单中的tocken,故,会被服务器拒绝访问
2. Security自动配置了普通请求的CSRF攻击
- 点开帖子详情页面,里面有很多提交表单请求
- 查看源代码,会发现隐藏的标签里传入了CSRF的tocken随机值
3. 异步请求自己配置csrf的tocken
HTML <meta>
标签
标签定义及使用说明
- 元数据(Metadata)是数据的数据信息。
- 标签提供了 HTML 文档的元数据。元数据不会显示在客户端,但是会被浏览器解析。
提示和注释
注意:<meta>
标签通常位于<head>
区域内。
注意: 元数据通常以 名称/值 对出现。
注意: 如果没有提供 name 属性,那么名称/值对中的名称会采用 http-equiv 属性的值。
自定义<meta>
标签,传入csdf元数据
- 访问该页面时,在此生成CSRF令牌,将Security中的元数据的传给页面,以key,value形式
<!-- 访问该页面时,在此生成CSRF令牌,将Security中的元数据的传给页面,以key,value形式-->
<!--csrf的key-->
<meta name="_csrf" th:content="${_csrf.headerName}">
<!--csrf的value-->
<meta name="_csrf_header" th:content="${_csrf.token}">
在发布请求的js文件中设置-将csrf令牌发给浏览器携带
// 发送AJAX请求之前,将CSRF令牌设置到请求的消息头中.
// 使用JQuery选择器,取指定meta标签中的content属性的值,赋值给变量key,value
var header = $("meta[name='_csrf_header']").attr("content");
var token = $("meta[name='_csrf']").attr("content");
// 在发布异步请求前,先对请求进行设置
$(document).ajaxSend(function (e, xhr, options) {
// xhr为发布异步请求的核心对象,设置请求头的key和value-设置完后可以放到浏览器中
// 发送请求时,会携带这个数据
xhr.setRequestHeader(header, token);
});
4. 关闭csrf
.and().csrf().disable()
- 关闭csrf功能
5. 测试结果显示
-
访问首页,mata标签携带数据
-
发布异步请求
6. 其他异步请求配置
- 同理,点赞、关注、取关、发送私信这些异步请求也要配置csrf,否则无法访问
点赞:
- 帖子详情页面
discuss.js
关注与取关
- 个人主页页面
- 关注和取关页面
profile.js
发送私信
- 私信页面
letter.html
- 和私信详情页面
letter.js