认证授权

Spring Security - 图1image.png

过滤器链

image.png

  • 贯穿于整个过滤器链始终有一个上下问对象 SecurityContext 和一个 Authentication 对象(登录认证的主体)
  • 一旦某一个该主体通过其中某一个过滤器的认证,Authentication 对象信息被填充,比如:Authentication #isAuthenticated=true 表示该主体通过验证。
  • 如果该主体通过了所有的过滤器,仍然没有被认证,在整个过滤器链的最后方有一个FilterSecurityInterceptor过滤器(虽然叫Interceptor,但它是名副其实的过滤器,不是拦截器)。判断Authentication对象的认证状态,如果没有通过认证则抛出异常,通过认证则访问后端API。
  • 之后进入响应阶段,FilterSecurityInterceptor 抛出的异常被 ExceptionTransactionFilter 对异常进行相应的处理。比如:用户名密码登录异常,会被引导到登录页重新登陆。
  • 如果是登陆成功,且没有任何异常,在 SecurityContextPersistenceFilter 中将 SecurityContext 放入session。下次再进行请求的时候,直接从 SecurityContextPersistenceFilter的session 中取出认证信息。从而避免多次重复认证。

登录过程

  1. 通过各种 Filters 拦截构建认证登录主体,返回一个 Authentication 实体 ```java public interface Authentication extends Principal, Serializable { Collection<? extends GrantedAuthority> getAuthorities();

    Object getCredentials();

    Object getDetails();

    Object getPrincipal();

    // 返回 true 时,该 认证实体 认证通过 boolean isAuthenticated();

    void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException; }

  1. 2. 使用 `AuthenticationManager `接口对登录认证主体进行 `#authenticate` 认证。
  2. ```java
  3. public interface AuthenticationManager {
  4. Authentication authenticate(Authentication authentication) throwsAuthenticationException;
  5. }

a. 实现类有 ProviderManager,是登录认证的核心类

  1. public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
  2. private List<AuthenticationProvider> providers;
  3. // other
  4. }

b. 里面有 AuthenticationProvider 列表,提供多种登录认证方式,只要有一个通过,那么这个 Authentication 实体就会认可,会填充对应的信息。 比如 Authentication#isAuthenticated() 就会返回 true

  1. public interface AuthenticationProvider {
  2. Authentication authenticate(Authentication var1) throws AuthenticationException;
  3. boolean supports(Class<?> var1);
  4. }
  1. 将认证完成的 Authtication对象( authenticate: true, 有授权列表 authority list, 和 username 信息)放入SecurityContext 上下文里面。后续的请求就直接从SecurityContextFilter中获得认证主体

登录验证后的处理

AuthenticationSuccessHandler

  • todo

    AuthenticationFailureHandler

  • todo

使用

SecurityConfig.java

  1. @Configuration
  2. public class SecurityConfig extends WebSecurityConfigurerAdapter {
  3. @Override
  4. protected void configure(HttpSecurity http) throws Exception {
  5. // <1.1> 开启 httpBasic 认证
  6. /*
  7. 将键入的 <user>:<password> 经过 Base64 加密
  8. 通过 header 头 Authorization 将 "Basic <加密字段>" 进行传递
  9. 注意:
  10. security:
  11. basic:
  12. # security 5.x 后,默认不是 HttpBasic 了,虚招自己配置中指定
  13. enabled: true
  14. http.httpBasic()
  15. .and()
  16. .authorizeRequests() // 对请求开始授权处理
  17. .anyRequest() // 要求 对于任何请求
  18. .authenticated(); // 要求 需要认证
  19. */
  20. // <1.2> 开启 formLogin(security 5.X 默认模式)
  21. http.csrf().disable() // 禁用 csrf 防御
  22. .formLogin() // formLogin, 要求 <Strong>post</Strong> 提交
  23. .loginPage("/login.html") // 未登录时,跳转登录页面
  24. .loginProcessingUrl("/auth/login") // 登录表单跳转的处理认证接口, security 会拦截该接口访问,不用自行实现
  25. .usernameParameter("username") // 登录表单用户名的 name 值,默认就是 username
  26. .passwordParameter("password") // 登录表单用户密码的 name 值,默认就是 password
  27. .defaultSuccessUrl("/index.html") // 登录成功跳转
  28. .failureForwardUrl("/login.html")
  29. .and()
  30. .authorizeRequests() // 对请求开始授权处理
  31. .antMatchers("/login.html", "/auth/login").permitAll() // 登录相关接口放开权限
  32. // 设置对应权限可访问的接口
  33. .antMatchers("/biz1.html", "/biz2.html")
  34. .hasAnyAuthority("ROLE_user", "ROLE_admin")
  35. .antMatchers("/syslog.html", "/sysuser.html")
  36. .hasAnyAuthority("ROLE_admin") // 等效于下面
  37. // .hasAnyRole("admin") // 设置对应角色可以访问的接口 由于角色是一种特殊的权限 等效于上面
  38. // .antMatchers("/test").hasAnyAuthority("sys:log", "sys:user") // sys:log 自定义权限id
  39. .anyRequest() // 除开上面设置的,其余一切请求
  40. .authenticated(); // 都需要登录
  41. }
  42. /**
  43. * 配置 角色和 权限
  44. *
  45. * @param auth
  46. * @throws Exception
  47. */
  48. @Override
  49. protected void configure(AuthenticationManagerBuilder auth) throws Exception {
  50. auth.inMemoryAuthentication()
  51. .withUser("admin")
  52. .password(passwordEncoder().encode("admin"))
  53. // .authorities("sys:log", "sys:user) // 自定义权限id
  54. .roles("admin")
  55. .and()
  56. .withUser("user")
  57. .password(passwordEncoder().encode("user"))
  58. .roles("user")
  59. .and()
  60. .passwordEncoder(passwordEncoder()); // 配置加密器,上面是用加密器直接加密了密码,这里配置后可以用于解密
  61. }
  62. @Bean
  63. public PasswordEncoder passwordEncoder() {
  64. return new BCryptPasswordEncoder();
  65. }
  66. /**
  67. * 放开静态资源
  68. *
  69. * @param web
  70. */
  71. @Override
  72. public void configure(WebSecurity web) {
  73. //将项目中静态资源路径开放出来
  74. web.ignoring().antMatchers("/css/**", "/fonts/**", "/img/**", "/js/**");
  75. }
  76. }

自定义 成功/失败 处理逻辑

  • 当登录成功的时候,是由 AuthenticationSuccessHandler 进行登录结果处理,默认跳转到 defaultSuccessUrl 配置的路径对应的资源页面(一般是首页 index.html)。
  • 当登录失败的时候,是由 AuthenticationfailureHandler 进行登录结果处理,默认跳转到 failureUrl 配置的路径对应的资源页面(一般是登录页 login.html)。

    自定义成功跳转

  • 可以实现 AuthenticationSuccessHandler 接口的子类 SavedRequestAwareAuthenticationSuccessHandler 类,这个类会记住用户上一次请求的资源路径。

image.png

  1. @Component
  2. public class MyAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
  3. private String loginType = "JSON";
  4. private static final ObjectMapper mapper = new ObjectMapper();
  5. @Override
  6. public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
  7. if ("JSON".equalsIgnoreCase(loginType)) {
  8. response.setContentType("application/json;charset=UTF-8");
  9. response.getWriter().write(mapper.writeValueAsString(ResResult.success("success")));
  10. } else {
  11. // 非 json 会跳转默认登录页
  12. super.onAuthenticationSuccess(request, response, authentication);
  13. }
  14. }
  15. }

自定义失败跳转

  • 继承 SimpleUrlAuthenticationFailureHandler 类。该类中默认实现了登录验证失败的跳转逻辑,即登陆失败之后回到登录页面。

    1. ![image.png](https://cdn.nlark.com/yuque/0/2020/png/367873/1582550515006-c3d5fa6d-643e-4e40-a8cc-f858b0d5b7b8.png#align=left&display=inline&height=211&name=image.png&originHeight=211&originWidth=480&size=45820&status=done&style=none&width=480)
  1. @Component
  2. public class MyAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
  3. private String loginType = "JSON";
  4. private static final ObjectMapper mapper = new ObjectMapper();
  5. @Override
  6. public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
  7. if ("JSON".equalsIgnoreCase(loginType)) {
  8. response.setContentType("application/json;charset=UTF-8");
  9. response.getWriter().write(
  10. mapper.writeValueAsString(ResResult.fail("账号或者密码错误"))
  11. );
  12. } else {
  13. response.setContentType("text/html;charset=UTF-8");
  14. super.onAuthenticationFailure(request, response, exception);
  15. }
  16. }
  17. }

配置到 SecurityConfig

  1. @Configuration
  2. public class SecurityConfig extends WebSecurityConfigurerAdapter {
  3. // 注入自定义跳转 Handler
  4. @Autowired
  5. private MyAuthenticationSuccessHandler successHandler;
  6. @Autowired
  7. private MyAuthenticationFailureHandler failureHandler;
  8. @Override
  9. protected void configure(HttpSecurity http) throws Exception {
  10. http.csrf().disable()
  11. .formLogin()
  12. .loginPage("/login.html")
  13. .loginProcessingUrl("/auth/login")
  14. .usernameParameter("username")
  15. .passwordParameter("password")
  16. .successHandler(successHandler)
  17. .failureHandler(failureHandler)
  18. // 自定义 handler 和默认 handler 二选一
  19. // .defaultSuccessUrl("/index.html") // 登录成功跳转
  20. // .failureForwardUrl("/login.html")
  21. // other
  22. }

session 和 cookie

Spring Security - 图5

Session

session 创建策略

  1. @Configuration
  2. public class SecurityConfig extends WebSecurityConfigurerAdapter {
  3. @Override
  4. protected void configure(HttpSecurity http) throws Exception {
  5. // other config
  6. // session 创建策略
  7. /*
  8. 该配置只能控制Spring Security如何创建与使用session,而不是控制整个应用程序。
  9. 即使不指定,应用程序本身可能会创建session, 而一般spring应用的session管理交由Spring Session进行
  10. 导致 spring security 还是用到了session
  11. */
  12. http.sessionManagement()
  13. .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED);
  14. }
  15. // other
  16. }

session 会话超时配置

  • 两种配置
  1. Springboot 配置

    1. server:
    2. servlet:
    3. session:
    4. # 默认 30m
    5. timeout: 15m
    6. spring:
    7. session:
    8. # 最少 1
    9. timeout: 15m
  2. Spring Session

    • 优先级更高
      1. session.setTime();
  • session 超时处理

    • 可以设置超时跳转页面
    • 注意该跳转页面需要 permitAll() 进行配置

      1. @Configuration
      2. public class SecurityConfig extends WebSecurityConfigurerAdapter {
      3. @Override
      4. protected void configure(HttpSecurity http) throws Exception {
      5. // other config
      6. // 非法超时session跳转页面,需要 permitAll() 进行配置
      7. http.sessionManagement()
      8. .invalidSessionUrl("/invalidSession.html");
      9. }
      10. // other
      11. }

会话固化存储设置

  1. @Configuration
  2. public class SecurityConfig extends WebSecurityConfigurerAdapter {
  3. @Override
  4. protected void configure(HttpSecurity http) throws Exception {
  5. // other config
  6. // <默认> 旧会话失效,新会话赋值旧会话的属性
  7. http.sessionManagement()
  8. .sessionFixation().migrateSession();
  9. // 原始会话不会无效
  10. http.sessionManagement()
  11. .sessionFixation().none();
  12. // 将创建一个干净的会话,而不会复制旧会话中的任何属性
  13. http.sessionManagement()
  14. .sessionFixation().newSession();
  15. }
  16. // other
  17. }

限制登录数和下线处理

  1. public class SecurityConfig extends WebSecurityConfigurerAdapter {
  2. @Override
  3. protected void configure(HttpSecurity http) throws Exception {
  4. // other config
  5. // session 限制登录数量
  6. http.sessionManagement()
  7. .maximumSessions(1) // 同一用户最大登录数
  8. .maxSessionsPreventsLogin(false) // session 保护策略 true: 已登录无法再登录 false: 可以多次登录,但是之前登录会下线
  9. .expiredSessionStrategy(new CustomExpiredSessionStrategy()); // 自定义 session 下线(超时) 处理策略
  10. }
  11. // other
  12. }
  1. /**
  2. * 自定义session被下线(超时)之后的处理策略。
  3. */
  4. public class CustomExpiredSessionStrategy implements SessionInformationExpiredStrategy {
  5. private ObjectMapper objectMapper = new ObjectMapper();
  6. @Override
  7. public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException {
  8. // Map -> Json
  9. String json = objectMapper.writeValueAsString(ResResult.fail("您的登录已经超时或者已经在另一台机器登录,您被迫下线。"
  10. + event.getSessionInformation().getLastRequest()));
  11. //输出JSON信息的数据
  12. event.getResponse().setContentType("application/json;charset=UTF-8");
  13. event.getResponse().getWriter().write(json);
  14. // 或者是跳转html页面,url代表跳转的地址
  15. // redirectStrategy.sendRedirect(event.getRequest(), event.getResponse(), "url");
  16. }
  17. }

Cookie

  1. server:
  2. servlet:
  3. session:
  4. cookie:
  5. # true 表示浏览器脚本无法访问 cookie
  6. http-only: true
  7. # true 表示只有 https 连接才能携带 cookie, 测试记得关掉
  8. secure: true

自定义加载用户数据

  • Spring Security 通过 UserDetailsService#loadUserByUserName() 加载 UserDetailsgetter

    1. public interface UserDetails extends Serializable {
    2. //获取用户的权限集合
    3. Collection<? extends GrantedAuthority> getAuthorities();
    4. //获取密码
    5. String getPassword();
    6. //获取用户名
    7. String getUsername();
    8. //账号是否没过期
    9. boolean isAccountNonExpired();
    10. //账号是否没被锁定
    11. boolean isAccountNonLocked();
    12. //密码是否没过期
    13. boolean isCredentialsNonExpired();
    14. //账户是否可用
    15. boolean isEnabled();
    16. }
  1. // Getter 通过实现 UserDetails 来重写
  2. @Setter
  3. @ToString
  4. public class MyUserDetails implements UserDetails {
  5. private String password; //密码
  6. private String username; //用户名
  7. private Boolean accountNonExpired; //是否没过期
  8. private Boolean accountNonLocked; //是否没被锁定
  9. private Boolean credentialsNonExpired; //是否没过期
  10. private Boolean enabled; //账号是否可用
  11. private Collection<? extends GrantedAuthority> authorities; //用户的权限集合
  12. @Override
  13. public Collection<? extends GrantedAuthority> getAuthorities() {
  14. return authorities;
  15. }
  16. @Override
  17. public String getPassword() {
  18. return password;
  19. }
  20. @Override
  21. public String getUsername() {
  22. return username;
  23. }
  24. // 下面默认都是 true
  25. @Override
  26. public boolean isAccountNonExpired() {
  27. return accountNonExpired == null;
  28. }
  29. @Override
  30. public boolean isAccountNonLocked() {
  31. return accountNonLocked == null;
  32. }
  33. @Override
  34. public boolean isCredentialsNonExpired() {
  35. return credentialsNonExpired == null;
  36. }
  37. @Override
  38. public boolean isEnabled() {
  39. return enabled == null;
  40. }
  41. }
  • 有不需要的逻辑灵活处理
  • 实现 UserDetailsService ,重写 loadUserByUsername() ,填充 UserDetails 实现类

    • 填充权限、角色
    • 其中角色需要加上 ROLE_前缀,因为角色是一种特殊的权限,在Spring Security 我们可以使用 hasRole (角色标识)表达式判断用户是否具有某个角色,决定他是否可以做某个操作;通过hasAuthority (权限标识)表达式判断是否具有某个操作权限。) ```java @Component public class MyUserDetailsService implements UserDetailsService { private static final Logger log = LoggerFactory.getLogger(MyUserDetailsService.class);

      @Autowired private RBACMapper rbacMapper;

      @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 1. 查询到用户的信息 UserDO userDO = rbacMapper.getUserInfoByUserName(username);

      if (Objects.isNull(userDO)) {

      1. throw new UsernameNotFoundException("没有该用户");

      }

      MyUserDetails myUserDetails = new MyUserDetails();

      BeanUtils.copyProperties(userDO, myUserDetails); log.info(“init myUserDetails {}”, myUserDetails);

      // 2. 查询角色信息 List roles = rbacMapper.getUserRolesByUserName(username);

      // 3. 查询权限信息 List authorities = rbacMapper.getUserPermitByUserName(username);

      // 3.1 由于 Spring Security 将角色当作权限,需要补充 ROLE_ 前缀 roles = roles.stream()

      1. .map(role->"ROLE_" + role)
      2. .collect(Collectors.toList());

      // 3.2 并且作为权限填充进权限列表中 authorities.addAll(roles);

      log.info(“final myUserDetails {}”, myUserDetails);

      // 4. 利用 Security 提供的工具类修改下权限列表类型 // 注意参数需要工具类需要逗号切分,所以要补充逗号, myUserDetails.setAuthorities(

      1. AuthorityUtils.commaSeparatedStringToAuthorityList(
      2. String.join(",", authorities)
      3. )

      ); return myUserDetails; } }

  1. <a name="zEw7g"></a>
  2. ### 注册
  3. ```java
  4. @Configuration
  5. public class SecurityConfig extends WebSecurityConfigurerAdapter {
  6. @Autowired
  7. private MyUserDetailsService myUserDetailsService;
  8. @Override
  9. protected void configure(AuthenticationManagerBuilder builder) throws Exception {
  10. // 可以配置多个 DetailsService
  11. builder.userDetailsService(myUserDetailsService)
  12. .passwordEncoder(passwordEncoder());
  13. }
  14. @Bean("passwordEncoder")
  15. public PasswordEncoder passwordEncoder(){
  16. return new BCryptPasswordEncoder();
  17. }
  18. // other
  19. }

实现登录

  • 之前配置了 .loginProcessingUrl("/auth/login") 即需要把所需的字段发送到这个地址即可
    • 所需字段是 usernamepassword (根据 UsernamePasswordAuthenticationFilter 的要求)
  • 然后配置登录成功的跳转页面 或者 handler 即可
    • 登录成功就会执行对应的逻辑
  • 即将登录的过程交给 Spring Security

**

自定义权限检查

  • access(权限表达式函数)
权限表达式函数 描述
hasRole([role]) 用户拥有指定的角色时返回true (Spring security默认会带有ROLE_前缀),去除前缀参考
hasAnyRole([role1,role2]) 用户拥有任意一个指定的角色时返回true
hasAuthority([authority]) 拥有某资源的访问权限时返回true
hasAnyAuthority([auth1,auth2]) 拥有某些资源其中部分资源的访问权限时返回true
permitAll 返回true
denyAll 返回false
anonymous 当前用户是anonymous时返回true
rememberMe 当前用户是rememberMe用户返回true
authentication 当前登录用户的authentication对象
fullAuthenticated 当前用户既不是anonymous也不是rememberMe用户时返回true
hasIpAddress('192.168.1.0/24') 请求发送的IP匹配时返回true

使用自带的权限表达式

  1. .antMatchers("/biz1.html", "/biz2.html")
  2. .access("hasAnyAuthority('ROLE_user', 'ROLE_admin')")
  3. // 等效于这句
  4. // .hasAnyAuthority("ROLE_user", "ROLE_admin")
  • 并且可以使用 and 等逻辑符号,灵活性更高
    1. .access("hasAnyAuthority('ROLE_user') or hasAnyAuthority('ROLE_admin')")

使用自定义 bean 逻辑

  • 创建一个 bean

    1. @Component("checkPermit")
    2. public class CheckPermit {
    3. private AntPathMatcher antPathMatcher = new AntPathMatcher();
    4. /**
    5. * 判断用户是否具有该request资源的访问权限
    6. * 即假设 UserDetails 的 authority 属性存储的是 url
    7. */
    8. public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
    9. Object principal = authentication.getPrincipal();
    10. if (principal instanceof UserDetails) {
    11. UserDetails userDetails = (UserDetails) principal;
    12. // 这里是直接判断网页 uri 是否为当前用户的持有的权限列表的一部分
    13. List<GrantedAuthority> authorityList = AuthorityUtils
    14. .commaSeparatedStringToAuthorityList(request.getRequestURI());
    15. return userDetails.getAuthorities().contains(authorityList.get(0));
    16. }
    17. return false;
    18. }
    19. }
  • 加载该 bean 的逻辑

    1. .antMatchers("/demo.html")
    2. // 配置校验器
    3. // 参数必须是 request 和 authentication, 源码要求
    4. .access("@checkPermit.hasPermission(request, authentication)")

    传入额外参数

  • 如果需要传入除了 request、authentication 之外的参数

  • 比如在 antPattern 中定义 /user/{userId} 带有 PathVariable 的参数,需要在对应的权限表达式中用 #{} 包裹
    1. .antMatchers("/user/{userId}")
    2. // 配置校验器
    3. // 参数必须是 request 和 authentication, 源码要求
    4. .access("@checkPermit.hasPermission2(request, authentication, #{userId})")

方法级别权限检查

  • 需要开启方法界别权限检查功能
    1. @Configuration
    2. @EnableGlobalMethodSecurity(prePostEnabled = true)
    3. public class SecurityConfig extends WebSecurityConfigurerAdapter {
    4. // other
    5. }
  1. @PreAuthorize() 进入方法前进行权限检查

    1. /**
    2. * Annotation for specifying a method access-control expression which will be evaluated to
    3. * decide whether a method invocation is allowed or not.
    4. *
    5. * @author Luke Taylor
    6. * @since 3.0
    7. */
    8. @Target({ ElementType.METHOD, ElementType.TYPE })
    9. @Retention(RetentionPolicy.RUNTIME)
    10. @Inherited
    11. @Documented
    12. public @interface PreAuthorize {
    13. /**
    14. * @return the Spring-EL expression to be evaluated before invoking the protected
    15. * method
    16. */
    17. String value();
    18. }
  2. @PreFilter() 进入方法前进行参数过滤

    1. @Target({ ElementType.METHOD, ElementType.TYPE })
    2. @Retention(RetentionPolicy.RUNTIME)
    3. @Inherited
    4. @Documented
    5. public @interface PreFilter {
    6. /**
    7. * @return the Spring-EL expression to be evaluated before invoking the protected
    8. * method
    9. */
    10. String value();
    11. /**
    12. * @return the name of the parameter which should be filtered (must be a non-null
    13. * collection instance) If the method contains a single collection argument, then this
    14. * attribute can be omitted.
    15. */
    16. String filterTarget() default "";
    17. }
  3. @PostAuthorize() 在方法执行后再进行权限验证,适合根据返回值结果进行权限验证。

    1. /**
    2. * Annotation for specifying a method access-control expression which will be evaluated
    3. * after a method has been invoked.
    4. *
    5. * @author Luke Taylor
    6. * @since 3.0
    7. */
    8. @Target({ ElementType.METHOD, ElementType.TYPE })
    9. @Retention(RetentionPolicy.RUNTIME)
    10. @Inherited
    11. @Documented
    12. public @interface PostAuthorize {
    13. /**
    14. * @return the Spring-EL expression to be evaluated after invoking the protected
    15. * method
    16. */
    17. String value();
    18. }
  4. @PostFilter() 针对返回结果进行过滤,特别适用于集合类返回值,过滤集合中不符合表达式的对象。 ```java @Target({ ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @Inherited @Documented public @interface PostFilter { /**

    • @return the Spring-EL expression to be evaluated after invoking the protected
    • method */ String value(); }
  1. <a name="mvfqC"></a>
  2. ## 记住我
  3. <a name="OcMYs"></a>
  4. ### 快速使用
  5. - 前端
  6. ```html
  7. <label for=""><input type="checkbox" name="remember-me">记住密码</label>
  • 后端

    1. http.rememberMe()
    2. .rememberMeParameter("remember-me") // 设置前端 checkbox 的 name,默认要求 remember-me
    3. .rememberMeCookieName("remember-me-name") // 成功返回 cookie,默认名为 remember-me
    4. .tokenValiditySeconds(60*20); // 记住我过期时间,默认 2周

    实现原理

  • 当我们登陆的时候,除了用户名、密码,我们还可以勾选remember-me。

  • 如果我们勾选了remember-me,当我们登录成功之后服务端会生成一个Cookie返回给浏览器,这个Cookie的名字默认是remember-me;值是一个token令牌。
  • 当我们在有效期内再次访问应用时,经过RememberMeAuthenticationFilter,读取Cookie中的token进行验证。验正通过不需要再次登录就可以进行应用访问。

    代码原理

  • RememberMeAuthenticationFilter 在 Spring Security过滤器链中处于整体偏后的位置

  • token 生成

    1. public class TokenBasedRememberMeServices extends AbstractRememberMeServices {
    2. // other
    3. // token 生成
    4. protected String makeTokenSignature(long tokenExpiryTime, String username, String password) {
    5. String data = username + ":" + tokenExpiryTime + ":" + password + ":" + this.getKey();
    6. MessageDigest digest;
    7. try {
    8. digest = MessageDigest.getInstance("MD5");
    9. } catch (NoSuchAlgorithmException var8) {
    10. throw new IllegalStateException("No MD5 algorithm available!");
    11. }
    12. return new String(Hex.encode(digest.digest(data.getBytes())));
    13. }

token 固化

退出登录

快速使用

  • 前端

    1. <button><a href="/logout">退出</a></button>
  • 后端

    1. http.logout();

默认流程

  • 当前 session 失效,核心功能,将访问权限的回收。
  • 删除当前用户的 remember-me 功能信息
  • 清除当前的 SecurityContext
  • 重定向到登录页面(loginPage 配置项指定的页面)

自定义登出配置

  • 简单版本 ```java http.logout() .logoutUrl(“/byte”) // 显式配置前端登出要跳转的地址,默认就是 /logout // .logoutSuccessUrl(“/logoutSuccess.html”) // 显式配置登出成功后,要跳转的地址, 默认跳转登录页 .deleteCookies(“JSESSIONID”); // 配置登录成功后,要删除的 cookies
  1. - 配置登出 handler
  2. - 注意优先级
  3. ```java
  4. http.logout()
  5. .logoutUrl("/byte") // 显式配置前端登出要跳转的地址,默认就是 /logout
  6. // .logoutSuccessUrl("/logoutSuccess.html") // 显式配置登出成功后,要跳转的地址, 默认跳转登录页
  7. .deleteCookies("JSESSIONID") // 配置登录成功后,要删除的 cookies
  8. .logoutSuccessHandler(new MyLogoutSuccessHandler()); // 配置登出 handler,<strong>优先级效于 logoutSuccessUrl() </strong>
  1. public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
  2. @Override
  3. public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
  4. // do something
  5. // 可以配置跳转页面或者返回 json
  6. httpServletResponse.sendRedirect("/logoutSuccess.html");
  7. }
  8. }

验证逻辑

短信验证码

SmsValidateFilter

  • 通过继承 OncePerRequestFilter 实现一个Spring环境下的过滤器。
    • 其核心校验规则如下:
    1. 用户登录时手机号不能为空
    2. 用户登录时短信验证码不能为空
    3. 用户登陆时在 session/缓存中 中必须存在对应的校验谜底(获取验证码时存放的)
    4. 用户登录时输入的短信验证码必须和 谜底 中的验证码一致
    5. 用户登录时输入的手机号必须和 谜底中保存的手机号一致
    • 另外的要求
      • 用户登录时输入的手机号必须是系统注册用户的手机号,并且唯一
      • 也可以登录就注册
  • 为获取验证码接口 permitAll() 或者其他权限校验规则

SmsCodeAuthenticationFilter

  • 由于默认的 UsernamePasswordAuthenticationFilter 是校验 username 和 password 两个字段
    • 需要自定义一个 AuthenticationFilter, 可以仿照 UsernamePasswordAuthenticationFilter , 重写它的 attemptAuthentication() 方法
  • 由于 AuthenticationFilter 将登录信息封装为一个 token, 通过 Authenticationmanager 来利用多个 XXXAuthenticationProvider 去调用 XXXUserService 来获取用户信息,来验证该 token
    • 需要自定义一个 AuthenticationToken 来传递登录信息,可以实现 AbstractAuthenticationToken,仿照 UsernamePasswordAuthenticationToken 来写
    • 需要自定义一个 AuthenticationFilter,可以仿照 DaoAuthenticationProvider ,实现AuthenticationProvider 来写
    • 需要自定义一个 AuthenticationUserService 和一个 UserDetails 来获取用户信息
  • TODO

前后端分离

jwt

安全性

  • 避免网络劫持,一般用 HTTP 的 header 传递 JWT,所以使用 HTTPS 传输更加安全。在网络层面避免了JWT的泄露。
  • secret 是存放在服务器端的,所以只要应用服务器不被攻破,理论上JWT是安全的。因此要保证服务器的安全。
  • 定期更换 secret 并且保证 secret 的复杂度,避免被暴力破解