Spring Security
介绍
-
简介
- Spring Security是一个专注与为Java应用程序提供身份认证和授权的框架,它的强大之处在于它可以轻松扩展以满足自定义的需求。
-
特征
- 对身份的认证和授权提供全面的、可扩展的支持。
- 防止各种攻击,如会话固定攻击、点击劫持、csrf攻击等。
- 支持与Servelt API、Spring MVC等Web技术集成。
-
原理
- 底层使用Filter(javaEE标准)进行拦截
- Filter–>DispatchServlet–>Interceptor–>Controller(后三者属于Spring MVC)
-
推荐学习网站:
www.spring4all.com
- 看几个核心的Filter源码
使用
- 1.导包:spring-boot-starter-security
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
废弃登录的拦截器
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private AlphaInterceptor alphaInterceptor;
@Autowired
private LoginTicketInterceptor loginTicketInterceptor;
//@Autowired
//private LoginRequiredInterceptor loginRequiredInterceptor;
@Autowired
private MessageInterceptor messageInterceptor;
@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");
}
}
-
2.新建配置类SecurityConfig
- 2.1 添加权限信息[CommunityConstant]
/** * 权限管理:普通用户 */ String AUTHORITY_USER = "user"; /** * 权限管理:管理员 */ String AUTHORITY_ADMIN = "admin"; /** * 权限管理:版主 */ String AUTHORITY_MODERATOR = "moderator";
- 2.2 SecurityConfig
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; //Security的配置类 @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter implements CommunityConstant { // 1. 忽略静态资源,不对静态资源进行拦截 @Override public void configure(WebSecurity web) throws Exception { // 忽略resources目录下的资源 web.ignoring().antMatchers("/resources/**"); } // 2. 授权 @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() // 2.1 对于以下列出的所有路径 .antMatchers( "/user/setting", // 用户设置 "/user/upload", // 用户文件上传 "/discuss/add", // 帖子发布 "/comment/add/**", // 评论发布 "/letter/**", // 私信相关内容 "/notice/**", // 通知相关内容 "/like", // 点赞 "/follow", // 加关注 "/unfollow" // 取消关注 ) // 只要有以下相关权限,都可以访问 [登录] .hasAnyAuthority( AUTHORITY_USER,// 权限: 普通用户 AUTHORITY_ADMIN,// 权限: 管理员 AUTHORITY_MODERATOR// 权限: 版主 ) // 2.2 对于以下列出的所有路径 .antMatchers( "/discuss/top", "/discuss/wonderful" ) // 只有具有以下列出的权限才可以访问 .hasAnyAuthority( AUTHORITY_MODERATOR// 权限: 版主 ) // 2.3 对于以下列出的所有路径 .antMatchers( "/discuss/delete", "/data/**" ) // 只有具有以下列出的权限才可以访问 .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 { // 如果请求x-requested-with 中头包含XMLHttpRequest 说明是异步请求 String xRequestedWith = request.getHeader("x-requested-with"); if ("XMLHttpRequest".equals(xRequestedWith)) { // 设置响应体是json 格式(因为是异步请求,所以返回内容要是json格式) 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)) { // 设置响应体是json 格式(因为是异步请求,所以返回内容要是json格式) 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"); } }
- 2.3 在UserService中添加获取用户权限的方法
//用于验证用户的权限: 根据userId获取用户的权限 public Collection<? extends GrantedAuthority> getAuthorities(int userId){ //1. 根据ID获取用户 User user = this.findUserById(userId); //2. 根据user判断用户的权限 List<GrantedAuthority> list = new ArrayList<>(); list.add(new GrantedAuthority() { @Override public String getAuthority() { // 根据用户的type确定用户的权限 switch (user.getType()){ case 1: return AUTHORITY_ADMIN; case 2: return AUTHORITY_MODERATOR; default: return AUTHORITY_USER; } } }); return list; }
- 2.4 在LoginTicketInterceptor
因为,SpringSecurity自己可以实现认证和权限,但是由于我们已经在系统中构建了认证的逻辑。因此,我们需要绕过SpringSecurity的认证,只利用权限。我们需要自己获取用户认证的结果,将认证结果存入到SecurityContext中,这是因为Security进行授权需要获取相关内容。
@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进行授权 // UsernamePasswordAuthenticationToken(用户,密码,权限) Authentication authentication = new UsernamePasswordAuthenticationToken( user,user.getPassword(),userService.getAuthorities(user.getId())); SecurityContextHolder.setContext(new SecurityContextImpl(authentication)); } } return true; } //清理权限 @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { hostHolder.clear(); // 清理SecurityContextHolder SecurityContextHolder.clearContext(); }
- 2.5 LoginController的退出方法中将用户认证的结果清除
@RequestMapping(path = "/logout", method = RequestMethod.GET) public String logout(@CookieValue("ticket") String ticket) { userService.logout(ticket); // 清理SecurityContextHolder SecurityContextHolder.clearContext(); return "redirect:/login"; }
- 3.CSRF配置
防止CSRF攻击的基本原理,以及表单、AJAX的相关配置。
- CSRF攻击:某网站盗取你的Cookie(ticket)凭证,模拟你的身份访问服务器。(发生在提交表单的时候)
- 首先浏览器已经与服务器建立连接,服务器已经将登录凭证存在浏览器的cookie中
- 当浏览器再次向服务器发起请求,一般是获取表单;服务器正常响应表单至浏览器
- 但是某网站盗取浏览器的Cookie(ticket)凭证,模拟你的身份访问服务器,提交表单,造成CSRF攻击
解决:
- Security会在表单里增加一个TOCKEN(自动生成),恶意网站虽然可以获取cookie但无法获取TOCKEN,因此保证了安全
- 但是异步请求Security无法在html文件生成CSRF令牌(异步不是通过请求体传数据,通过请求头)
- 发送AJAX请求之前,将CSRF令牌设置到请求的消息头中
解决:利用Security添加Tocken
-
3.1 处理异步请求
index.html
<!--访问该页面时,生成CSRF令牌--> <meta name="_csrf" th:content="${_csrf.token}"> <!--生成CSRF令牌的value--> <meta name="_csrf_header" th:content="${_csrf.headerName}"><!--生成CSRF令牌的key-->
index.js
//发送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); });
可以在config配置取消CSRF
// 取消CSRF .and().csrf().disable();