2.7 CSRF防护
CSRF:跨站请求伪造,通过伪造用户请求访问受信任的站点从而进行非法请求访问,是一种攻击手段。 Spring Security为了防止CSRF攻击,默认开启了CSRF防护,这限制了除了GET请
求以外的大多数方法。我们要想正常使用Spring Security需要突破CSRF防护。
解决方法一:关闭CSRF防护.
http.csrf().disable();
解决方法二:突破CSRF防护.
CSRF为了保证不是其他第三方网站访问,要求访问时携带参数名为_csrf值为令牌,令牌在服务端产生,如果携带的令牌和服务端的令牌匹配成功,则正常访问。
<form class="form" action="/login" method="post"> <!-- 在表单中添加令牌隐藏域 --> <input type="hidden" th:value="${_csrf.token}" name="_csrf" th:if="${_csrf}"/> <input type="text" placeholder="用户名" name="username"> <input type="password" placeholder="密码" name="password"> <button type="submit">登录</button> </form>
2.8 会话管理
用户认证通过后,有时我们需要获取用户信息,比如在网站顶部显示:欢迎您,XXX。Spring Security将用户信息保存在会话中,并提供会话管理,我们可以从SecurityContext
对象中获取用户信息,SecurityContext
对象与当前线程进行绑定。
package com.zj.controller; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class MyController { //获取当前登录的用户名 @RequestMapping("/name") public String getName(){ //1.获取会话对象 SecurityContext context = SecurityContextHolder.getContext(); //2.获取认证对象 Authentication authentication = context.getAuthentication(); //3.获取用户信息 UserDetails userDetails = (UserDetails) authentication.getPrincipal(); return userDetails.getUsername(); } }
2.9 认证成功后的处理方式
登录成功后,如果除了跳转页面还需要执行一些自定义代码时,如:统计访问量,推送消息等操作时,可以自定义登录成功处理器。
1、自定义登录成功处理器
public class MyLoginSuccessHandler implements AuthenticationSuccessHandler { @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { // 拿到登录用户的信息 UserDetails userDetails = (UserDetails)authentication.getPrincipal(); System.out.println("用户名:"+userDetails.getUsername()); System.out.println("一些操作..."); // 重定向到主页 response.sendRedirect("/main"); } }
2、在security配置类中修改配置
http.formLogin() // 使用表单登录 .loginPage("/login.html") // 自定义登录页面 .usernameParameter("username") // 表单中的用户名项 .passwordParameter("password") // 表单中的密码项 .loginProcessingUrl("/login") // 登录路径,表单向该路径提交,提交后自动执行UserDetailsService的方法 // .successForwardUrl("/main") //登录成功后跳转的路径 .successHandler(new MyLoginSuccessHandler()) //登录成功处理器 .failureForwardUrl("/fail"); //登录失败后跳转的路径
这样只有在控制台输出完成后才会跳转到主页面。
2.10 认证失败后的处理方式
登录失败后,如果除了跳转页面还需要执行一些自定义代码时,如:统计失败次数,记录日志等,可以自定义登录失败处理器。
1、自定义登录失败处理器
public class MyLoginFailureHandler implements AuthenticationFailureHandler { @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { System.out.println("记录失败日志"); response.sendRedirect("/fail"); } }
2、配置登录失败处理器
http.formLogin() // 使用表单登录 .loginPage("/login.html") // 自定义登录页面 .usernameParameter("username") // 表单中的用户名项 .passwordParameter("password") // 表单中的密码项 .loginProcessingUrl("/login") // 登录路径,表单向该路径提交,提交后自动执行UserDetailsService的方法 // .successForwardUrl("/main") //登录成功后跳转的路径 .successHandler(new MyLoginSuccessHandler()) //登录成功处理器 // .failureForwardUrl("/fail") //登录失败后跳转的路径 .failureHandler(new MyLoginFailureHandler()); //登录失败处理器 // 需要认证的资源 http.authorizeRequests() .antMatchers("/login.html").permitAll() // 登录页不需要认证 .antMatchers("/fail").permitAll() // 失败页不需要认证 .anyRequest().authenticated(); //其余所有请求都需要认证
2.11 退出登录
在系统中一般都有退出登录的操作。退出登录后,Spring Security进行了以下操作:
- 清除认证状态
- 销毁HttpSession对象
- 跳转到登录页面
在Spring Security中,退出登录的写法如下:
1、配置退出登录的路径和退出后跳转的路径
// 退出登录配置 http.logout() .logoutUrl("/logout") // 退出登录路径 .logoutSuccessUrl("/login.html") // 退出登录后跳转的路径 .clearAuthentication(true) //清除认证状态,默认为true .invalidateHttpSession(true); // 销毁HttpSession对象,默认为true
2、在网页中添加退出登录超链接
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>主页面</title> </head> <body> <h1>主页面</h1> <a href="/logout">退出登录</a> </body> </html>
2.12 退出成功处理器
我们也可以自定义退出成功处理器,在退出后清理一些数据,写法如下:
1、自定义退出成功处理器
public class MyLogoutSuccessHandler implements LogoutSuccessHandler { @Override public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException, IOException { System.out.println("清除一些数据..."); response.sendRedirect("/login.html"); } }
2、配置退出成功处理器
// 退出登录配置 http.logout() .logoutUrl("/logout") // 退出登录路径 // .logoutSuccessUrl("/login.html") // 退出登录后跳转的路径 .clearAuthentication(true) //清除认证状态,默认为true .invalidateHttpSession(true) // 销毁HttpSession对象,默认为 true .logoutSuccessHandler(new MyLogoutSuccessHandler()); //自定义退出成功处理器
2.13 Remember Me
Spring Security中Remember Me为“记住我”功能,即下次访问系统时无需重新登录。当使用“记住我”功能登录后,Spring Security会生成一个令牌(一串字符串),令牌一方面保存到数据库中,另一方面生成一个叫remember-me
的Cookie保存到客户端。之后客户端访问项目时自动携带令牌,不登录即可完成认证。
1、编写“记住我”配置类
package com.zj.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl; import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository; import javax.annotation.Resource; import javax.sql.DataSource; @Configuration public class RememberMeConfig { @Resource private DataSource dataSource; //连接数据库的工具类 @Bean public PersistentTokenRepository getPersistentTokenRepository() { //1.为spring security 自带的令牌控制器设置数据源 JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl(); jdbcTokenRepository.setDataSource(dataSource); //2.自动创建令牌表(第一次启动时需要,第二次启动时注释就行) jdbcTokenRepository.setCreateTableOnStartup(true); return jdbcTokenRepository; } }
2、修改Security配置类
@Resource private UserDetailsService userDetailsService; @Resource private PersistentTokenRepository persistentTokenRepository; //记住我配置 http.rememberMe() .userDetailsService(userDetailsService) //登录逻辑 .tokenRepository(persistentTokenRepository) //令牌工具类 .tokenValiditySeconds(30);// 保存30秒
3、在登录页面添加“记住我”复选框
<form class="form" action="/login" method="post"> <input type="text" placeholder="用户名" name="username"> <input type="password" placeholder="密码" name="password"> <!--name必须是 remember-me--> <input type="checkbox" name="remember-me" value="true"/>记住我</br> <button type="submit">登录</button> </form>
4、启动项目,数据库创建出token表。
5、将记住我配置中的创建表的配置注释掉。
//2.自动创建令牌表(第一次启动时需要,第二次启动时注释就行) // jdbcTokenRepository.setCreateTableOnStartup(true);
三、Spring Security授权
3.1 RBAC
授权即认证通过后,系统给用户赋予一定的权限,用户只能根据权限访问系统中的某些资源。RBAC是业界普遍采用的授权方式,它有两种解释:
Role-Based Access Control
基于角色的访问控制,即按角色进行授权。比如在企业管理系统中,主体角色为总经理可以查询企业运营报表。逻辑为:
if(主体.hasRole("总经理角色")){ 查询运营报表 }
如果查询企业运营报表的角色变化为总经理和股东,此时就需要修改判断逻辑代码:
if(主体.hasRole("总经理角色") || 主体.hasRole("股东角色")){ 查询运营报表 }
此时我们可以发现,当需要修改角色的权限时就需要修改授权的相关代码,系统可扩展性差。
Resource-Based Access Control
基于资源的访问控制,即按资源(或权限)进行授权。比如在企业管理系统中,用户必须 具有查询报表权限才可以查询企业运营报表。逻辑为:
if(主体.hasPermission("查询报表权限")){ 查询运营报表 }
这样在系统设计时就已经定义好查询报表的权限标识,即使查询报表所需要的角色变化为总经理和股东也不需要修改授权代码,系统可扩展性强。该授权方式更加常用。
3.2 权限表设计
用户和权限的关系为多对多,即用户拥有多个权限,权限也属于多个用户,所以建表方式如下:
这种方式需要指定用户有哪些权限,如:张三有查询工资的权限,即在用户权限中间表中添加一条数据,分别记录张三和查询工资权限ID。但在系统中权限数量可能非常庞大,如果一条一条添加维护数据较为繁琐。所以我们通常的做法是再加一张角色表:
用户角色,角色权限都是多对多关系,即一个用户拥有多个角色,一个角色属于多个用户;一个角色拥有多个权限,一个权限属于多个角色。这种方式需要指定用户有哪些角色,而角色又有哪些权限。
如:张三拥有总经理的角色,而总经理拥有查询工资、查询报表的权限,这样张三就拥有了查询工资、查询报表的权限。这样管理用户时只需管理少量角色,而管理角色时也只需要管理少量权限即可。
接下来我们创建五张表: