SpringSecurity

简介

  • 简介

    SpringSecurity 是一个功能强大且高度可定制的身份验证和访问控制框架。SpringSecurity 致力于为Java 应用程序提供身份验证和授权的能力。像所有Spring 项目一样,SpringSecurity 的真正强大之处在于它可以轻松扩展以满足定制需求的能力

  • 核心功能

    • 用户认证(Authentication): 验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程
    • 用户授权(Authorization): 验证某个用户是否具有权限操作执行某个操作。在一个系统中,不同用户所有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,有的用户既能读取,又能修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色对应一系列的权限

创建起步初识项目

  • 添加依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    <dependencies>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
    </dependency>

    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
    </dependency>

    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    </dependencies>
  • 版本信息

    版本确定
  • 创建控制器

    1
    2
    3
    4
    5
    6
    7
    8
    @RestController
    @CrossOrigin
    public class HelloController {
    @GetMapping("/hello")
    public String hello() {
    return "Hello SpringSecurity";
    }
    }
  • 启动访问

    添加security,访问会出现如下页面 用户名:user,密码来自于控制台

过滤器链

  • 调式查看

    客户端->目标资源
  • 功能介绍

    • WebAsyncManagerIntegrationFilter: 将SecurityContext 集成到SpringMVC 中用于管理异步请求处理的WebAsyncManager
    • SecurityContextPersistenceFilter: 在当前会话中填充SecurityContext,SecurityContext Security 的上下文对象,里面包含了当前用户的认证及权限信息等
    • HeadWriteFilter: 向请求的Header 中添加信息
    • LogoutFilter:匹配URL /logout 的请求,清除认证信息,实现用户注销功能
    • UsernamePasswordAuthenticationFilter: 认证操作的过滤器,用于匹配URL /login POST 请求做拦截,校验表单的用户名和密码
    • RequestCacheAwareFilter: 用于缓存HttpServletRequest
    • SecurityContextHolderAwareRequestFilter: 用于封装ServletRequest,ServletRequest 具备更多功能
    • AnonymousAuthenticationFilter: 对于未登录情况下的处理,SecurityContextHolder 中认证信息为空,则会创建一个匿名用户存入到SecurityContextHolder
    • SessionManagementFilter: 限制统一用户开启多个会话
    • ExceptionTranslationFilter: 异常过滤器,用来处理在认证授权过程中抛出的异常
    • FilterSecurityInterceptor: 获取授权信息,根据SecurityContextHolder 中存储的用户信息判断用户是否有权限访问
  • 过滤器的加载过程

    SpringBoot 在整合SpringSecurity 项目时会自动配置DelegatingFilterProxy 过滤器,若非Springboot 工程、则需要手动配置该过滤器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException {
    Filter delegateToUse = this.delegate;
    if (delegateToUse == null) {
    synchronized(this.delegateMonitor) {
    delegateToUse = this.delegate;
    if (delegateToUse == null) {
    WebApplicationContext wac = this.findWebApplicationContext();
    if (wac == null) {
    throw new IllegalStateException("No WebApplicationContext found: no ContextLoaderListener or DispatcherServlet registered?");
    }

    delegateToUse = this.initDelegate(wac);
    }

    this.delegate = delegateToUse;
    }
    }

    this.invokeDelegate(delegateToUse, request, response, filterChain);
    }

认证方式

认证的概念

所谓的认证,就是用来判断系统中是否存在某用户,并判断用户的身份是否是合法的过程,解决的其实是用户登录的问题,认证的存在,是为了保护系统中的隐私数据与资源,只有合法的用户才可以访问系统中的资源

HTTP 基本认证
  • 基本认证

    HTTP 基本认证是在RFC2616 标准中定义的一种认证模式,它以一种很简单的方式与用户进行交互。HTTP 基本认证可以分为如下4 个步骤

    1. 客户端首先发起一个未携带认证信息的请求
    2. 然后服务端返回一个401 Unauthorized 的响应信息,并在WWW-Authentication 头部中说明认证形式: 当进行HTTP 基本认证时,WWW-Authentication 会被设置为Basic relam="被保护的页面"
    3. 接下来客户端会收到401 Unauthorized 响应信息,并弹出一个对话框,询问用户名和密码。当用户输入后,客户端会将用户名和密码使用冒号进行拼接并用Nasr64 编码,然后将其放入到请求的Authorization 头部并发送给服务器
    4. 最后服务器对客户端发来的信息进行解码得到用户名和密码,并对该信息进行校验判断是否正确,最终给客户端返回响应内容

    HTTP 基本认证是一种无状态的认证方式,与表单认证相比,HTTP 基本认证是一种基于HTTP 层面的认证方式,无法携带Session 信息,也就无法实现Remember-Me 功能,另外,用户名和密码在传递时仅做了一次简单的Base64 编码,几乎等同于明文传输,极易被进行密码窃听和重放攻击。所以在实际开发中,很少会使用这种认证方式来进行安全校验。

Form 表单认证
HTTP 摘要认证
  • 概念

    HTTP 摘要认证和HTTP 基本认证一样,也是在RFC2616 中定义的一种认证方式,他的出现是为了弥补HTTP 基本认证存在的安全隐患,但该认证方式也并不是很安全.HTTP 摘要认证会使用对通信双方都可知的口令进行校验,且最终以密文的形式来传输数据,所以相对于基本认证,稍微安全了一些

登录页-自定义用户名和密码

  • 配置

    1
    2
    3
    4
    5
    6
    spring:
    security:
    user:
    name: coder-itl
    # setPassword
    password: root
    1
    2
    3
    4
    5
    6
    7
    8
    // password: root 的源码
    public void setPassword(String password) {
    if (StringUtils.hasLength(password)) {
    // 控制台生成密码的开关
    this.passwordGenerated = false;
    this.password = password;
    }
    }

关闭验证功能

  • 排除Secuirty 的配置,让他不启用

    1
    2
    3
    4
    5
    6
    7
    8
    // 启动类
    @SpringBootApplication(exclude = {SecurityAutoConfiguration.class})
    public class SpringSecurity01Application {
    public static void main(String[] args) {
    SpringApplication.run(SpringSecurity01Application.class, args);
    }

    }

登录页-使用内存中的用户信息

  1. 使用: WebSecurityConfigurerAdapter 控制安全管理的内容

    需要做的使用: 继承WebSecurityConfigurerAdapter,重写方法。实现自定义的认证信息

  2. 创建security 的配置类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    package com.example.config;

    @EnableWebSecurity
    public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {
    // 在方法中配置 用户名和密码信息 作为登录的数据
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    PasswordEncoder passwordEncoder = passwordEncoder();
    auth.inMemoryAuthentication()
    .withUser("coder-itl").password(passwordEncoder.encode("coder-itl")).roles();
    auth.inMemoryAuthentication()
    .withUser("admin").password(passwordEncoder.encode("admin")).roles();
    auth.inMemoryAuthentication()
    .withUser("root").password(passwordEncoder.encode("root")).roles();

    }

    // 创建密码的加密类
    @Bean
    public PasswordEncoder passwordEncoder() {
    // 创建 PasswordEncoder 的实现类 实现类是加密算法
    return new BCryptPasswordEncoder();
    }
    }

    错误: 密码不能为明文信息,解决方法为添加加密
  • 基于角色Role 的身份认证,同一个用户可以有不同的角色。同时可以开启对方法级别的认证

    1
    2
    3
    @EnableGlobalMethodSecurity: 启用方法级别的认证
    prePostEnabled: 默认是 false
    true: 表示可以使用 @PreAuthorize 注解 和 @PostAuthorize
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    // 控制器编写: 基于内存中的用户信息认证
    @RestController
    public class HelloController {
    @GetMapping("/hello")
    public String hello() {
    return "Hello SpringSecurity";
    }

    // 指定 normal 和 admin 都可以访问的方法
    @GetMapping("/helloUser")
    @PreAuthorize(value = "hasAnyRole('admin','normal')")
    public String helloCommonUser() {
    return "normal 和 admin 都可以访问的方法";
    }

    // 仅 admin 可以访问的方法
    @GetMapping("/helloAdmin")
    @PreAuthorize(value = "hasAnyRole('admin')")
    public String helloAdmin() {
    return "仅 admin 可以访问的方法";
    }
    }

  • 细节注意

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    package com.example.oauth2.config;

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
    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.EnableWebSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.security.crypto.password.PasswordEncoder;

    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
    /**
    * authorizeRequests: 授权请求
    * authenticated: 认证
    */
    http.authorizeRequests().anyRequest().authenticated().and()
    // 开启表单验证
    .formLogin().loginPage("/login.html").permitAll()
    // 当登录成功后,是否指定跳转到首页
    .defaultSuccessUrl("/index.html", true)
    // post 请求的登录接口
    .loginProcessingUrl("/login")
    // 登录失败,用户名或密码错误
    .failureUrl("/error.html")
    // 登录时携带的用户名和免密的表单的键
    .usernameParameter("username").passwordParameter("password")
    // 注销
    .and().logout().logoutUrl("/logout")
    // 注销成功后的页面
    .logoutSuccessUrl("/login.html")
    // 删除自定义的 cookie
    .deleteCookies("myCookie")
    // 关闭 csrf 防护功能,否则登录不成功
    .and().csrf().disable();
    }

    /**
    * 配置请求那些资源时不需要做认证
    *
    * @param web
    * @throws Exception
    */
    @Override
    public void configure(WebSecurity web) throws Exception {
    web.ignoring().mvcMatchers("/js/**", "/css/**", "/image/**");
    }

    /* 基于内存的用户名和密码 */
    @Bean
    PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
    }
    // TODO: 如果注入 passwordEncoder 会产生循环依赖,需要通过配置来进行打破循环依赖
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    // 生成密码的密文
    String pwd = passwordEncoder.encode("user");
    // 设置用户名和密码
    auth.inMemoryAuthentication().withUser("user").password(pwd).roles("admin");
    }
    }

    1
    2
    3
    4
    5
    # 当注入了 PasswordEncoder 时
    spring:
    main:
    # 打破循环依赖
    allow-circular-references: true

角色- RBAC

  • 认证

    身份认证就是判断一个用户是否为合法用户的处理过程。Spring Security 中支持多种不同方式的认证,但是无论开发者使用那种方式认证,都不会影响授权功能使用。因为Spring Security 很好的做到了认证和授权解饿

  • 授权

    即访问控制,控制谁能访问哪些资源。简单的理解授权就是根据系统提前设置好的规则,给用户分配可以访问某一个资源的权限,用户根据自己所具有的权限,去执行响应操作。

  • RBAC

    ​ RBAC 是基于角色的访问控制(Role-Based Access Controle),在 RBAC 中,权限与角色相关联,用户通过成为适当角色的成员而得到这些角色的权限。这就极大地简化了权限的管理。这样管理都是层级相互依赖的,权限赋予给角色,而把角色又赋予给用户

    ​ 其基本思想是对系统操作的各种权限不是直接授予具体的用户,而是在用户集合之间建立一个角色集合。每一种角色对应一组相应的权限。一旦被用户分配到了适当的角色后,该用户就拥有此角色的所有操作权限。这样做的好处是,不必在每次创建用户时都进行分配权限的操作,只要分配用户相应的角色即可,而且角色的权限变更比用户的权限变更要少得多,这样就简化用户的权限管理,减少系统的开销

  • RBAC 中设计表

    1. 用户表: 用户认证(登录用到的表),用户名、密码、是否启用,是否锁定等信息
    2. 角色表: 定义角色信息,角色名称、角色的描述
    3. 用户和角色的关系表:用户和角色是多对多的关系。一个用户可以又多个角色,一个角色可以有多个用户
    4. 权限表:角色和权限的关系表,权限(角色可以有那些权限)

认证的接口和类

  • UserDetails 接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19

    public interface UserDetails extends Serializable {
    // 权限的集合
    Collection<? extends GrantedAuthority> getAuthorities();

    // 密码
    String getPassword();
    // 用户名
    String getUsername();
    // 账号是否过期
    boolean isAccountNonExpired();
    // 账号是否锁定
    boolean isAccountNonLocked();
    // 证书是否过期
    boolean isCredentialsNonExpired();
    // 是否启用
    boolean isEnabled();
    }

  • UserDetails 的实现类User

    1
    2
    3
    4
    import org.springframework.security.core.userdetails.User;

    // 自定义类可以实现 UserDetails 接口,作为系统中的用户类。这个类可以交给 spring security 使用

    User
  • UserDetailsService 接口

    主要作用: 获取用户信息,得到的是UserDetails 对象。一般项目中都需要自定义类实现这个接口,从数据库中获取数据

    1
    2
    3
    4
    5
    // 一个方法要实现
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    // 根据用户名,获取用户信息
    }

自动配置分析

  • 自动配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @ConditionalOnDefaultWebSecurity
    static class SecurityFilterChainConfiguration {
    SecurityFilterChainConfiguration() {
    }
    // 创建过滤器 SecurityFilterChain
    @Bean
    @Order(2147483642)
    SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
    ((ExpressionUrlAuthorizationConfigurer.AuthorizedUrl)http.authorizeRequests().anyRequest()).authenticated(); // 表单认证
    http.formLogin();
    // 早期的认证方式
    http.httpBasic();
    return (SecurityFilterChain)http.build();
    }
    }

生成默认登录页的流程分析

  • 流程图

    流程分析
    • 关键说明
      1. 请求/hello 接口,在引入spring security 之后会经过一系列过滤器
      2. 在请求到达FilterSecurityInterceptor 时,发送请求并未认证。请求拦截下来,并抛出AccessDeniedException 异常
      3. 抛出AccessDeniedException 的异常会被ExceptionTranslationFilter 捕获,这个Filter 中会调用LoginUrlAuthenticationEntryPoint#commence 方法返回给客户端302(重定向),要求客户端进行重定向到/login 页面
      4. 客户端发送/login 请求
      5. /login 请求会再次被拦截其中DefaultLoginPageGeneratingFilter 拦截到,并在拦截其中返回生成登录页面

自定义认证

  • 需求

    • index 公共资源
    • hello 受限资源
  • 控制器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    /**
    * 受保护资源
    */
    @RestController
    public class HelloController {
    @GetMapping("/hello")
    public String hello() {
    return "hello spring security";
    }
    }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    /**
    * 公共资源
    */
    @RestController
    public class IndexController {
    @GetMapping("/index")
    public String index() {
    return "hello index.........";
    }
    }

  • 创建配置类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15

    // WebSecurityConfigurerAdapter 在 2.7 过时
    @Configuration
    public class WebSecurityConfigure extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    // 放行资源在前 首先资源在后
    http.authorizeRequests()
    // 匹配放行资源
    .mvcMatchers("/index").permitAll()
    // 其他皆是受保护资源
    .anyRequest().authenticated().and().formLogin();
    }
    }

    1
    2
    3
    4
    # 说明
    - permitAll() 代表放行该资源 该资源为公共资源 无需认证和授权可以直接访问
    - anyRequest().authenticated() 代表所有请求,必须认证后才能访问
    - formLogin() 代表开启表单认证

自定义登录页面

  • vue 页面

    自定义页面
    • 修改默认访问页面

      1
      2
      // 配置类后续追加该配置 指定默认的登录页面
      .loginPage("http://localhost:8081/#/login");
  • Thymeleaf 使用

    • 添加依赖

      1
      2
      3
      4
      <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-thymeleaf</artifactId>
      </dependency>
    • 关闭缓存

      1
      2
      3
      spring:
      thymeleaf:
      cache: false
    • 自定义登录页面

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      <!doctype html>
      <html lang="en" xmlns:th="http://www.thymeleaf.org">
      <head>
      <meta charset="UTF-8">
      <meta name="viewport"
      content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
      <meta http-equiv="X-UA-Compatible" content="ie=edge">
      <title>自定义用户登录页面</title>
      <style>
      form {
      background-color: orange;
      }
      </style>
      </head>
      <body>
      <form th:action="@{/login}" method="post">
      <p>
      用户名: <input type="text" name="username">
      </p>
      <p>
      密码: <input type="password" name="password">
      </p>
      <input type="submit" value="登录">
      </form>

      </body>
      </html>
    • 配置类中放行请求

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30

      // WebSecurityConfigurerAdapter 在 2.7 过时
      @Configuration
      public class WebSecurityConfigure extends WebSecurityConfigurerAdapter {
      @Override
      protected void configure(HttpSecurity http) throws Exception {
      // 放行资源在前 首先资源在后
      http.authorizeRequests()
      // 匹配放行资源
      .mvcMatchers("/login").permitAll()
      .mvcMatchers("/index").permitAll()
      // 其他皆是受保护资源
      .anyRequest().authenticated().and().formLogin()
      // 指定默认的登录页面 一旦自定义登录页面以后必须只能登录 url
      .loginPage("/login")
      // 指定处理登录请求 url
      .loginProcessingUrl("/login")
      // 指定input name属性值如果不是 username 和 password 时
      .usernameParameter("username")
      .passwordParameter("password")

      // 传统开发: 认证成功后跳转(只能选其一) forward(不改变路由):始终在认证成功后跳转到指定请求 | redirect(改变路由) 根据上一次保存的请求进行成功跳转,可通过修改参数强制跳转 .defaultSuccessUrl("/index",true)
      .successForwardUrl("/index")
      .defaultSuccessUrl("/index")

      // 禁止 csrf 跨站请求保护
      .and().csrf().disable();
      }
      }

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      // 前后端分离

      /**
      * 自定义认证成功之后的处理
      */
      public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
      @Override
      public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
      Map<String, Object> result = new HashMap<>();
      result.put("msg", "登录成功");
      result.put("status", "200");
      result.put("authentication", authentication);
      response.setContentType("application/json;charset=UTF-8");
      String json = new ObjectMapper().writeValueAsString(result);
      response.getWriter().println(json);
      }
      }

      1
      2
      // 配置类添加如下配置
      .successHandler(new MyAuthenticationSuccessHandler())
      页面响应json 数据
    • 控制器

      1
      2
      3
      4
      5
      6
      7
      8
      @Controller
      public class LoginController {
      @GetMapping("/login")
      public String login() {
      // login.html
      return "login";
      }
      }
    • 获取失败信息

      1
      2
      3
      4
      5
      6
      7
      8
      9
      // 配置类中

      // forwald 错误信息存储在 request 作用域
      .failureForwardUrl("/login")
      // redirect 错误信息村粗在 session 作用域
      .failureUrl("/login")

      存储的键: SPRING_SECURITY_LAST_EXCEPTION

    • 前后端错误信息处理

      1
      .failureHandler(new MyAuthenticationFailureHandler())
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      /**
      自定义认证失败
      */
      public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
      @Override
      public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
      Map<String, Object> result = new HashMap<>();
      result.put("msg", "登录失败: " + exception.getMessage());
      result.put("status", 500);
      response.setContentType("application/json;charset=UTF-8");
      String json = new ObjectMapper().writeValueAsString(result);
      response.getWriter().println(json);
      }
      }
      输入错误的密码

注销

  • 配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    .and()
    // logoutUrl | logoutRequestMatcher 选其一
    .logout()
    .logoutUrl("/logout")
    // 自定义退出路径与请求方式
    .logoutRequestMatcher(new OrRequestMatcher(
    new AntPathRequestMatcher("/aa","GET"),
    // POST 请求的退出
    new AntPathRequestMatcher("/bb","POST")
    ))
    // 退出成功后跳转的页面 默认/login
    .logoutSuccessUrl("/login")
    .logoutSuccessHandler(new MyLogoutSuccessHandler())

  • 前端后端分离

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
    Map<String, Object> result = new HashMap<>();
    result.put("msg", "注销成功,当前认证对象为: " + authentication);
    result.put("status", "200");
    response.setContentType("application/json;charset=UTF-8");
    String json = new ObjectMapper().writeValueAsString(result);
    response.getWriter().println(json);
    }
    }
    自定义注销

完整配置顺序

  • 顺序

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    @Configurable
    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    // 安全管理配置
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
    // 所有请求
    .anyRequest()
    // 都需要认证
    .authenticated()
    // 认证方式为表单认证
    .and().formLogin()
    // 登录成功后
    .successHandler(new MyAuthenticationSuccessHandler())
    // 登录失败后
    .failureHandler(new MyAuthenticationFailureHandler())
    .and().logout().logoutUrl("/logout")
    .and()
    .logout().logoutUrl("/logout")
    // 退出成功后
    .logoutSuccessHandler(new MyLogoutSuccessHandler())
    // 禁用跨站
    .and().csrf().disable();
    }
    }

WebSecurityHttpSecurity

  • 配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    /**
    * 配置请求那些资源时不需要做认证
    *
    * @param web
    * @throws Exception
    */
    @Override
    public void configure(WebSecurity web) throws Exception {
    web.ignoring().mvcMatchers("/js/**", "/css/**", "/image/**");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {}
  • WebSecurity: 在这个类里定义了一个securityFilterChainBuilders 集合,可以同时管理多个SecurityFilterChain 过滤器链,当WebSecurity 在执行时,会构建出一个名为springSecurityFilterChain Spring BeanFilterChainProxy 代理类,他的作用是定义那些请求客户忽略安全控制,那些请求必须接受安全控制,以及在合适的时候清除 SecurityContext 以避免内存泄漏,同时也可以用来定义请求防火墙和请求拒绝处理器,也可以在这里开启Spring Security Debug 模式

  • HttpSecurity: HttpSecurity 用来构建包含一系列的过滤器链SecurityFilterChain,平常我们的配置就是围绕这个SecurityFilterChain 进行

自定义数据源认证(一)

  • 用户表

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    create table sys_user
    (
    id int not null primary key auto_increment comment '主键id',
    username varchar(100) comment '用户名',
    password varchar(100) comment '密码',
    realname varchar(200) comment '实名用户名',
    isexpire int comment '是否过期',
    isenable int comment '是否禁用',
    islock int comment '是否锁定',
    iscredentials int comment '证书是否可用',
    createtime date comment '创建时间',
    logintime date comment '登录时间'
    ) comment '用户表';
  • 角色表

    1
    2
    3
    4
    5
    6
    7
    create table sys_role
    (
    id int not null primary key auto_increment,
    rolename varchar(255) comment '角色名称',
    rolememo varchar(255) comment '角色描述'
    ) comment '角色表';

  • 角色用户对应关系表

    1
    2
    3
    4
    5
    create table sys_user_role
    (
    userid int,
    roleid int
    ) comment '用户角色关系对应表';
    表信息
    在这里插入图片描述
  • 创建项目

    • 添加依赖

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      <dependencies>
      <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-security</artifactId>
      </dependency>
      <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
      </dependency>
      <dependency>
      <groupId>org.mybatis.spring.boot</groupId>
      <artifactId>mybatis-spring-boot-starter</artifactId>
      <version>2.2.2</version>
      </dependency>

      <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <scope>runtime</scope>
      </dependency>
      <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-configuration-processor</artifactId>
      <optional>true</optional>
      </dependency>
      <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <optional>true</optional>
      </dependency>
      <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
      </dependency>
      <dependency>
      <groupId>org.springframework.security</groupId>
      <artifactId>spring-security-test</artifactId>
      <scope>test</scope>
      </dependency>
      </dependencies>
    • 配置

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      server:
      port: 8081

      spring:
      datasource:
      driver-class-name: com.mysql.cj.jdbc.Driver
      username: root
      password: root
      url: jdbc:mysql://localhost:3306/security?useUnicode=true&serverTimezone=GMT&characterEncoding=UTF-8&useSSL=false
      security:
      user:
      name: root
      password: root


      mybatis:
      type-aliases-package: com.example.entity
      mapper-locations: com/example/mapper/*.Mapper
      configuration:
      map-underscore-to-camel-case: true


      logging:
      level:
      com.example.mapper: debug

    • 自定义User 实现UserDetails

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      package com.example.entity;

      @Data
      @NoArgsConstructor
      @AllArgsConstructor
      public class SysUser implements UserDetails {
      private Integer id;
      private String username;
      private String password;
      private String realname;

      // 是否过期
      private boolean isExpired;
      // 是否锁定
      private boolean isLocked;
      // 凭证是否有效
      private boolean isCredentials;
      // 是否启用
      private boolean isEnabled;

      private Date createTime;
      private Date loginTime;

      private List<GrantedAuthority> authorities;


      @Override
      public Collection<? extends GrantedAuthority> getAuthorities() {
      return authorities;
      }

      @Override
      public String getPassword() {
      return password;
      }

      @Override
      public String getUsername() {
      return username;
      }

      @Override
      public boolean isAccountNonExpired() {
      return isExpired;
      }

      @Override
      public boolean isAccountNonLocked() {
      return isLocked;
      }

      @Override
      public boolean isCredentialsNonExpired() {
      return isCredentials;
      }

      @Override
      public boolean isEnabled() {
      return isEnabled;
      }
      }

    • 角色

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      package com.example.entity;

      @Data
      @NoArgsConstructor
      @AllArgsConstructor
      public class SysRole {
      private Integer id;
      private String rolename;
      private String rolememo;
      }

    • 实现service

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      package com.example.service;

      @Slf4j
      @Service
      public class JdbcUSerServiceDetail implements UserDetailsService {
      @Resource
      private SysUserMapper sysUserMapper;
      @Resource
      private SysRoleMapper sysRoleMapper;

      @Override
      public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
      log.info("username: [{}]", username);
      SysUser sysUser = null;
      // 角色信息集合
      List<GrantedAuthority> authorities = new ArrayList<>();
      // 1. 根据用户名查询用户信息
      if (username != null) {
      sysUser = sysUserMapper.selectSysUser(username);
      log.error("sysUser: [{}]", sysUser.toString());
      String roleName = "";
      if (sysUser != null) {

      List<SysRole> roleList = sysRoleMapper.selectRoleByUser(sysUser.getId());
      log.error("roleList: [{}]", roleList.toString());
      for (SysRole role : roleList) {
      roleName = role.getRolename();
      GrantedAuthority authority = new SimpleGrantedAuthority("ROLE_" + roleName);
      authorities.add(authority);
      }
      }
      sysUser.setAuthorities(authorities);
      }
      return sysUser;
      }

      }

    • Security 配置类

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      package com.example.config;

      import javax.annotation.Resource;

      @Slf4j
      @Configuration
      @EnableWebSecurity
      public class CustomerWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
      @Resource
      private UserDetailsService userDetailsService;


      @Override
      protected void configure(HttpSecurity http) throws Exception {
      log.error("config HttpSecurity...........");
      http.authorizeRequests().antMatchers("/index").permitAll()
      .antMatchers("/access/read").hasRole("READ")
      .antMatchers("/access/user").hasRole("USER")
      .antMatchers("/access/admin").hasRole("ADMIN")
      // 其他皆是受保护资源
      .anyRequest().authenticated().and().formLogin();
      }


      @Override
      protected void configure(AuthenticationManagerBuilder auth) throws Exception {
      // 实现自定义 new BCryptPasswordEncoder()
      auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
      }
      }

    • 创建mapper

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      <?xml version="1.0" encoding="UTF-8" ?>
      <!DOCTYPE mapper
      PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
      "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
      <mapper namespace="com.example.mapper.SysUserMapper">

      <resultMap id="userMap" type="sysUser">
      <id column="id" property="id"/>
      <!-- column: 数据库列明 property: 实体字段名 -->
      <result column="username" property="username"/>
      <result column="password" property="password"/>
      <result column="realname" property="realname"/>
      <result column="isexpire" property="isExpired"/>
      <result column="isenable" property="isEnabled"/>
      <result column="islock" property="isLocked"/>
      <result column="iscredentials" property="isCredentials"/>
      <result column="createtime" property="createTime"/>
      <result column="logintime" property="loginTime"/>
      </resultMap>


      <!-- int insertSysUser(SysUser user); -->
      <insert id="insertSysUser" parameterType="sysUser">
      insert into sys_user(username,
      password,
      realname,
      isenable,
      islock,
      isexpire,
      iscredentials,
      createtime,
      logintime)
      values (#{username},
      #{password},
      #{realname},
      #{isEnabled},
      #{isLocked},
      #{isExpired},
      #{isCredentials},
      #{createTime},
      #{loginTime})
      </insert>

      <!-- SysUser selectSysUser(String username); -->
      <select id="selectSysUser" parameterType="string" resultMap="userMap">
      select *
      from sys_user
      where username = #{username}
      </select>

      </mapper>

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      <?xml version="1.0" encoding="UTF-8" ?>
      <!DOCTYPE mapper
      PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
      "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
      <mapper namespace="com.example.mapper.SysRoleMapper">

      <!-- List<SysRole> selectRoleByUser(Integer userId); -->
      <select id="selectRoleByUser" parameterType="integer" resultType="sysRole">
      select r.id, r.rolename, r.rolememo
      from sys_user_role ur,
      sys_role r
      where ur.roleid = r.id
      and ur.userid = #{userId}
      </select>
      </mapper>

    • 初始化信息

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
      import org.springframework.security.crypto.password.PasswordEncoder;

      @PostConstruct
      public void init() {
      PasswordEncoder pwdEncoder = new BCryptPasswordEncoder();
      Date curDate = new Date();
      List<GrantedAuthority> list = new ArrayList<>();
      // 参数角色名称 必须以"ROLE_"开头 后面加上自定义的角色名称
      // GrantedAuthority read = new SimpleGrantedAuthority("ROLE_" + "READ");
      // GrantedAuthority user = new SimpleGrantedAuthority("ROLE_" + "USER");
      GrantedAuthority admin = new SimpleGrantedAuthority("ROLE_" + "ADMIN");
      // list.add(read);
      // list.add(user);
      // list.add(admin);
      // 普通用户
      // SysUser lisiRead = new SysUser(1, "lisi", pwdEncoder.encode("lisi"), "李四", true, true, true, true, curDate, curDate, list);
      // SysUser zsUser= new SysUser(2, "zs", pwdEncoder.encode("zs"), "张三", true, true, true, true, curDate, curDate, list);
      SysUser coderitlAdmin = new SysUser(3, "coder-itl", pwdEncoder.encode("coder-itl"), "CODER-ITL", true, true, true, true, curDate, curDate, list);
      // sysUserMapper.insertSysUser(lisiRead);
      // sysUserMapper.insertSysUser(zsUser);
      sysUserMapper.insertSysUser(coderitlAdmin);
      }
    • 源文件

      https://gitee.com/coder-itl/spring-security.git

    • 静态页面跳转

      1
      2
      3
      4
      5
      6
      7
      @Controller
      public class IndexController {
      @GetMapping("/index")
      public String index() {
      return "forward:/index.html";
      }
      }
      index 实现跳转
      • 静态页面

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        <!DOCTYPE html>
        <html lang="en">
        <head>
        <meta charset="UTF-8">
        <title>Title</title>
        </head>
        <body>
        <a href="/access/read">只读-李四</a>
        <a href="/access/user">普通用户-张三</a>
        <a href="/access/admin">CODER-ITL 管理员</a>
        </body>
        </html>
      • 控制器

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        package com.example.controller;

        import lombok.extern.slf4j.Slf4j;
        import org.springframework.web.bind.annotation.GetMapping;
        import org.springframework.web.bind.annotation.RestController;

        @Slf4j
        @RestController
        public class MyController {
        @GetMapping("/access/user")
        public String accessZS() {
        return "zs and admin.....";
        }

        @GetMapping("/access/read")
        public String accessRD() {
        return "lisi.....";
        }


        @GetMapping("/access/admin")
        public String accessAM() {
        return "admin.....";
        }
        }

自定义数据源认证(二)

  • spring-security 中的User

    User
    在这里插入图片描述
  • 设计表结构

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    -- 用户表
    CREATE TABLE `user` (
    `id` int NOT NULL AUTO_INCREMENT COMMENT '用户ID',
    `username` varchar(32) DEFAULT NULL COMMENT '用户名',
    `password` varchar(255) DEFAULT NULL COMMENT '密码',
    `enabled` tinyint(1) DEFAULT NULL COMMENT '是否启用',
    `accountNonExpired` tinyint(1) DEFAULT NULL COMMENT '账户是否过期',
    `accountNonLocked` tinyint(1) DEFAULT NULL COMMENT '账户是否锁定',
    `credentialsNonExpired` tinyint(1) DEFAULT NULL COMMENT '凭证是否过期',
    PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

    1
    2
    3
    4
    -- 插入用户数据
    insert into user values (1, 'root', '{noop}123', 1, 1, 1, 1)
    insert into user values (2, 'admin', '{noop}123', 1, 1, 1, 1)
    insert into user values (3, 'coder-itl', '{noop}123', 1, 1, 1, 1)
    1
    2
    3
    4
    5
    6
    CREATE TABLE `role` (
    `id` int NOT NULL AUTO_INCREMENT COMMENT '角色ID',
    `name` varchar(32) DEFAULT NULL COMMENT '角色名称',
    `name_zh` varchar(32) DEFAULT NULL COMMENT '角色中文名称',
    PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
    1
    2
    3
    insert into role values (1,'ROLE_product','商品管理员');
    insert into role values (2,'ROLE_admin','系统管理员');
    insert into role values (3,'ROLE_user','用户管理员');
    1
    2
    3
    4
    5
    6
    7
    8
    CREATE TABLE `user_role` (
    `id` int NOT NULL AUTO_INCREMENT,
    `uid` int DEFAULT NULL,
    `rid` int DEFAULT NULL,
    PRIMARY KEY (`id`),
    KEY `uid` (`uid`),
    KEY `rid` (`rid`)
    ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
    1
    2
    3
    4
    insert into user_role values (1, 1, 1);
    insert into user_role values (2, 1, 2);
    insert into user_role values (3, 2, 2);
    insert into user_role values (4, 3, 3);
    表结构
  • 自定义User

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    package com.example.entity;

    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.authority.SimpleGrantedAuthority;
    import org.springframework.security.core.userdetails.UserDetails;

    import java.util.*;

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public class User implements UserDetails {
    private Integer id;
    private String username;
    private String password;
    private Boolean enabled;
    private Boolean accountNonExpired;
    private Boolean accountNonLocked;
    private Boolean credentialsNonExpired;
    private List<Role> roles = new ArrayList<>();

    // 权限集合
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
    Set<SimpleGrantedAuthority> authorities = new HashSet<>();
    roles.forEach(role -> {
    SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(role.getName());
    authorities.add(simpleGrantedAuthority);
    });
    return authorities;
    }

    @Override
    public String getPassword() {
    return password;
    }

    @Override
    public String getUsername() {
    return username;
    }

    @Override
    public boolean isAccountNonExpired() {
    return accountNonExpired;
    }

    @Override
    public boolean isAccountNonLocked() {
    return accountNonLocked;
    }

    @Override
    public boolean isCredentialsNonExpired() {
    return credentialsNonExpired;
    }

    @Override
    public boolean isEnabled() {
    return enabled;
    }
    }

  • Role

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    package com.example.entity;

    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public class Role {
    private Integer id;
    private String name;
    private String nameZh;
    }

  • mapper

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
    PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="com.example.mapper.UserMapper">
    <!-- User findUserByUserName(String username); -->
    <select id="findUserByUserName" resultType="user">
    select *
    from user
    where username = #{username}
    </select>

    <!-- List<Role> getRoleByUid(Integer uid); -->
    <select id="getRoleByUid" resultType="role">
    select r.id, r.name, r.name_zh
    from role r,
    user_role ur
    where r.id = ur.uid
    and ur.uid = #{uid}
    </select>
    </mapper>

  • UserDetailsService 实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    package com.example.service;

    import com.example.entity.Role;
    import com.example.entity.User;
    import com.example.mapper.UserMapper;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.core.userdetails.UsernameNotFoundException;
    import org.springframework.stereotype.Service;
    import org.springframework.util.ObjectUtils;

    import java.util.List;

    @Service
    public class MyUserDetailsService implements UserDetailsService {

    private final UserMapper userMapper;

    @Autowired
    public MyUserDetailsService(final UserMapper userMapper) {
    this.userMapper = userMapper;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    // 根据用户名查询用户
    User user = userMapper.findUserByUserName(username);
    // 如果用户为空
    if (ObjectUtils.isEmpty(user)) throw new UsernameNotFoundException("用户名错误!");
    // 查询权限信息
    List<Role> roles = userMapper.getRoleByUid(user.getId());
    user.setRoles(roles);
    return user;
    }
    }

  • 配置SpringSecurity

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    package com.example.config;

    import com.example.service.MyUserDetailsService;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.authentication.AuthenticationManager;
    import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

    @Slf4j
    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    private MyUserDetailsService myUserDetailsService;

    @Autowired
    public SecurityConfig(final MyUserDetailsService myUserDetailsService) {
    this.myUserDetailsService = myUserDetailsService;
    }

    // 自定义 AuthenticationManager 推荐 并没有在工厂在暴露出来
    @Override
    protected void configure(AuthenticationManagerBuilder builder) throws Exception {
    log.error("自定义 AuthenticationManager: " + builder);
    builder.userDetailsService(myUserDetailsService);
    }

    // 作用: 用来将自定义 AuthenticationManager 在工厂中进行暴露 可以在任意位置注入
    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
    return super.authenticationManagerBean();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
    .anyRequest().authenticated().and().formLogin().and().csrf().disable();
    }
    }

传统 WEB 开发认证总结案例

  • SpringMVC 的自定义配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    package com.example.config;

    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

    /**
    * 对 springmvc 进行自定义配置: 可以省略部分控制器
    */
    @Configuration
    public class MvcConfigure implements WebMvcConfigurer {
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
    registry.addViewController("/login").setViewName("login");
    registry.addViewController("/index").setViewName("index");
    }
    }

  • 传统web Security 所有配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    package com.example.config;

    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

    @Configuration
    public class SecurityConfigure extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
    // 开启认证
    http.authorizeRequests()
    // 放行的资源
    .mvcMatchers("/login", "/index").permitAll()
    // 所有请求都需要认证
    .anyRequest().authenticated()
    // 表单认证 指定自定义登录页面
    .and().formLogin().loginPage("/login")
    .loginProcessingUrl("/login")
    // 指定用户名 和 密码的 name 属性值
    .usernameParameter("username")
    .passwordParameter("password")
    // 登录成功后跳转的页面
    .defaultSuccessUrl("/index")
    // 退出
    .and().logout()
    // 退出成功后的跳转(默认 login)
    .logoutSuccessUrl("/login")
    .and().csrf().disable();
    }
    }

前后端分离认证总结

  • 配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    package com.example.config;

    import com.example.filter.LoginFilter;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.MediaType;
    import org.springframework.security.authentication.AuthenticationManager;
    import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.core.userdetails.User;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.provisioning.InMemoryUserDetailsManager;
    import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

    import java.util.HashMap;
    import java.util.Map;

    @Configuration
    public class SecurityConfigure extends WebSecurityConfigurerAdapter {
    // 使用内存中的用户认证
    @Bean
    public UserDetailsService userDetailsService() {
    InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
    inMemoryUserDetailsManager.createUser(User.withUsername("root").password("{noop}123").roles("admin").build());
    return inMemoryUserDetailsManager;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userDetailsService());
    }

    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
    return super.authenticationManagerBean();
    }

    // 自定义 filter 交给工厂管理
    @Bean
    public LoginFilter loginFilter() throws Exception {
    LoginFilter loginFilter = new LoginFilter();
    // 接受指定的 json 用户名 key
    loginFilter.setUsernameParameter("username");
    // 接受指定的 json 密码 key
    loginFilter.setPasswordParameter("password");
    // 注入 authdicationManager
    loginFilter.setAuthenticationManager(authenticationManagerBean());
    // 认证成功后
    loginFilter.setAuthenticationSuccessHandler((req, resp, auth) -> {
    Map<String, Object> result = new HashMap<>();
    result.put("msg", "登录成功");
    // 状态码: 200
    resp.setStatus(HttpStatus.OK.value());
    result.put("authentication", auth);
    resp.setContentType("application/json;charset=UTF-8");
    String json = new ObjectMapper().writeValueAsString(result);
    resp.getWriter().println(json);
    });
    // 认证失败后
    loginFilter.setAuthenticationFailureHandler((req, resp, ex) -> {
    Map<String, Object> result = new HashMap<>();
    result.put("msg", "登录失败: " + ex.getMessage());
    // 服务器内部错误 状态码: 500
    resp.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
    resp.setContentType("application/json;charset=UTF-8");
    String json = new ObjectMapper().writeValueAsString(result);
    resp.getWriter().println(json);
    });
    return loginFilter;
    }

    // 配置安全
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
    // 所有请求都需要认证
    .anyRequest().authenticated().and().formLogin()
    // 退出
    .and()
    .exceptionHandling()
    .authenticationEntryPoint((req, resp, ex) -> {
    Map<String, Object> result = new HashMap<>();
    result.put("msg", "请认证之后再去处理....");
    result.put("status", HttpStatus.UNAUTHORIZED.value());
    resp.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
    String json = new ObjectMapper().writeValueAsString(result);
    resp.getWriter().println(json);
    })
    .and()
    .logout().logoutUrl("/logout")
    // 退出成功后
    .logoutSuccessHandler((req, resp, auth) -> {
    Map<String, Object> result = new HashMap<>();
    result.put("msg", "注销成功");
    result.put("authentication", auth.getPrincipal());
    resp.setContentType("application/json;charset=UTF-8");
    resp.setStatus(HttpStatus.OK.value());
    String json = new ObjectMapper().writeValueAsString(result);
    resp.getWriter().println(json);
    })
    // 禁用跨站
    .and().csrf().disable();
    // at: 用在某个 filter 替换过滤器链中哪个 filter
    // before: 放在过滤器链中那个 filter 之前
    // after: 放在过滤器链中那个 filter 之后
    http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class);
    }
    }

  • 控制器

    1
    2
    3
    4
    5
    6
    7
    8
    @RestController
    public class TestController {
    @GetMapping("/test")
    public String test() {
    return "test ok...........................";
    }
    }

  • 自定义前后端认证

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    package com.example.filter;

    import com.fasterxml.jackson.databind.ObjectMapper;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.http.MediaType;
    import org.springframework.security.authentication.AuthenticationServiceException;
    import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.util.Map;

    /**
    * 自定义前后端认证
    */
    @Slf4j
    public class LoginFilter extends UsernamePasswordAuthenticationFilter {
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) {
    log.error("attemptAuthentication: [{}]", "Override attemptAuthentication...................................");
    // 1. 判断请求方式是否是 POST
    if (!request.getMethod().equals("POST")) {
    throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
    }
    // 2. 判断 数据是否是 JSON 格式
    if (request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE)) {
    try {
    // 将请求体中的数据进行反序列化
    Map<String, String> userInfo = new ObjectMapper().readValue(request.getInputStream(), Map.class);
    String username = userInfo.get(getUsernameParameter());
    String password = userInfo.get(getPasswordParameter());
    log.error("username: [{}]", username);
    log.error("password: [{}]", password);
    UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
    this.setDetails(request, authRequest);
    return this.getAuthenticationManager().authenticate(authRequest);
    } catch (IOException e) {
    throw new RuntimeException(e);
    }
    }
    // 如果不是 JSON 格式数据,则调用传统方式进行认证
    return super.attemptAuthentication(request, response);
    }
    }

  • 访问测试

    访问成功 访问失败 注销成功
    在这里插入图片描述
  • 更改内存数据源为数据库

    • 实现service

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      package com.example.service;

      import com.example.entity.Role;
      import com.example.entity.User;
      import com.example.mapper.UserMapper;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.security.core.userdetails.UserDetails;
      import org.springframework.security.core.userdetails.UserDetailsService;
      import org.springframework.security.core.userdetails.UsernameNotFoundException;
      import org.springframework.stereotype.Service;
      import org.springframework.util.ObjectUtils;

      import java.util.List;

      @Service
      public class MyUserDetailsService implements UserDetailsService {

      private final UserMapper userMapper;

      @Autowired
      public MyUserDetailsService(final UserMapper userMapper) {
      this.userMapper = userMapper;
      }

      @Override
      public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
      // 根据用户名查询用户
      User user = userMapper.findUserByUserName(username);
      // 如果用户为空
      if (ObjectUtils.isEmpty(user)) throw new UsernameNotFoundException("用户名错误!");
      // 查询权限信息
      List<Role> roles = userMapper.getRoleByUid(user.getId());
      user.setRoles(roles);
      return user;
      }
      }

    • 配置修改

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      package com.example.config;

      @Configuration
      public class SecurityConfigure extends WebSecurityConfigurerAdapter {
      // 使用数据库中的用户认证
      private final MyUserDetailsService myUserDetailsService;

      @Autowired
      public SecurityConfigure(final MyUserDetailsService myUserDetailsService) {
      this.myUserDetailsService = myUserDetailsService;
      }
      @Override
      protected void configure(AuthenticationManagerBuilder auth) throws Exception {
      auth.userDetailsService(myUserDetailsService);
      }

      ...

      }

    • UserMapper.java

      1
      2
      3
      4
      5
      6
      @Repository
      public interface UserMapper {
      User findUserByUserName(String username);

      List<Role> getRoleByUid(Integer id);
      }
    • UserMapper.xml

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      <?xml version="1.0" encoding="UTF-8" ?>
      <!DOCTYPE mapper
      PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
      "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
      <mapper namespace="com.example.mapper.UserMapper">
      <!-- User findUserByUserName(String username); -->
      <select id="findUserByUserName" resultType="user">
      select *
      from user
      where username = #{username}
      </select>

      <!-- List<Role> getRoleByUid(Integer uid); -->
      <select id="getRoleByUid" resultType="role">
      select r.id, r.name, r.name_zh
      from role r,
      user_role ur
      where r.id = ur.uid
      and ur.uid = #{uid}

    • 数据源配置

    • 访问测试

      调试loadUserByUsername
      前后端只能通过其他工具测试,浏览器不支持 成功访问

添加认证验证码

  • 添加依赖

    1
    2
    3
    4
    5
    <dependency>
    <groupId>com.github.penggle</groupId>
    <artifactId>kaptcha</artifactId>
    <version>2.3.2</version>
    </dependency>
  • 自定义验证码生成

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    package com.example.controller;

    import lombok.extern.slf4j.Slf4j;
    import org.springframework.web.bind.annotation.CrossOrigin;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;

    import javax.imageio.ImageIO;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.awt.*;
    import java.awt.image.BufferedImage;
    import java.io.IOException;
    import java.io.OutputStream;
    import java.util.Random;

    @Slf4j
    @CrossOrigin
    @RestController
    @RequestMapping("/captcha")
    public class CaptchaController {
    // 图像宽度 150px
    private int width = 150;
    private int height = 50;
    // 图片内容起始位置
    private int drawY = 30;
    // 文字间隔
    private int space = 18;
    // 验证码文字个数
    private int charCount = 6;
    // 验证码内容数组
    private String[] code = {
    "A", "B", "C", "D", "E", "F", "G", "H", "K", "Z", "X", "L", "C", "V", "B", "N", "M", "Q", "W",
    "R", "T", "Y", "U", "I", "O", "P", "A", "B", "c", "d", "e", "f", "g", "h", "k", "z", "x",
    "l", "c", "v", "b", "n", "m", "q", "w", "r", "t", "y", "u", "i", "o", "p",
    "1", "2", "3", "4", "5", "6", "7", "8", "9", "0"
    };

    @GetMapping("/code")
    public void makeCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
    // 创建一个背景透明的图片 使用 rgb 表示颜色的
    BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
    // 获取画笔
    Graphics graphics = image.getGraphics();
    // 设置使用的画笔是白颜色
    graphics.setColor(Color.white);
    // 给 inage 画板都涂成白色的
    // fillRect(矩形)(矩形的起始x,矩形的起始y,width,height)
    graphics.fillRect(0, 0, width, height);
    // 将文字内容输出到图片上
    // 1. 创建一个字体
    Font font = new Font("宋体", Font.BOLD, 16);
    graphics.setFont(font);
    // 设置画笔颜色
    graphics.setColor(Color.black);
    // 2. 在画布上添加文字
    // graphics.drawString("中", 20, drawY);
    int ran = 0;
    int len = code.length;
    StringBuffer stringBuffer = new StringBuffer("");
    for (int i = 0; i < charCount; i++) {
    ran = new Random().nextInt(len);
    // 使用随机颜色绘制
    graphics.setColor(randomColor());
    stringBuffer.append(code[ran]);
    graphics.drawString(code[ran], (i + 1) * space, drawY);
    }
    // 将验证码存储到 session
    request.getSession().setAttribute("code", stringBuffer);
    // 绘制干扰线
    for (int i = 0; i < 6; i++) {
    graphics.setColor(randomColor());
    int dot[] = makeLineDot();
    graphics.drawLine(dot[0], dot[1], dot[2], dot[3]);
    }
    // 设置响应格式
    response.setContentType("image/png");
    // 缓存设置
    response.setHeader("Pragma", "no-cache");
    response.setHeader("Cache-Control", "no-cache");
    response.setDateHeader("Expires", 0);
    OutputStream outputStream = response.getOutputStream();
    ImageIO.write(image, "png", outputStream);
    outputStream.flush();
    outputStream.close();
    }

    /**
    * 生成随机颜色
    *
    * @return
    */
    private Color randomColor() {
    int r = new Random().nextInt(255);
    int g = new Random().nextInt(255);
    int b = new Random().nextInt(255);
    return new Color(r, g, b);
    }

    private int[] makeLineDot() {
    Random random = new Random();
    int x1 = random.nextInt(width / 2);
    int y1 = random.nextInt(height);

    int x2 = random.nextInt(width);
    int y2 = random.nextInt(height);
    return new int[]{x1, x2, y1, y2};
    }
    }

  • 传统WEB 验证码添加

    • 添加谷歌验证码配置

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      package com.example.config;

      import com.google.code.kaptcha.Producer;
      import com.google.code.kaptcha.impl.DefaultKaptcha;
      import com.google.code.kaptcha.util.Config;
      import org.springframework.context.annotation.Bean;
      import org.springframework.context.annotation.Configuration;

      import java.util.Properties;

      @Configuration
      public class KaptchaConfig {
      @Bean
      public Producer kaptcha() {
      Properties properties = new Properties();
      properties.setProperty("kaptcha.image.width", "150");
      properties.setProperty("kaptcha.image.height", "50");
      properties.setProperty("kaptcha.textproducer.char.string", "0123456789");
      properties.setProperty("kaptcha.textproducer.char.length", "4");
      Config config = new Config(properties);
      DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
      defaultKaptcha.setConfig(config);
      return defaultKaptcha;
      }
      }

    • 自定义过滤器

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      package com.example.filter;

      import com.example.exception.KaptchaNotMatchException;
      import com.fasterxml.jackson.databind.ObjectMapper;
      import lombok.extern.slf4j.Slf4j;
      import org.springframework.http.MediaType;
      import org.springframework.security.authentication.AuthenticationServiceException;
      import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
      import org.springframework.security.core.Authentication;
      import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
      import org.springframework.util.ObjectUtils;

      import javax.servlet.http.HttpServletRequest;
      import javax.servlet.http.HttpServletResponse;
      import java.io.IOException;
      import java.util.Map;

      /**
      * 自定义前后端认证
      */
      @Slf4j
      public class LoginFilter extends UsernamePasswordAuthenticationFilter {
      private static final String FORM_KAPTCHA_KEY = "code";
      private String kaptchaParameter = FORM_KAPTCHA_KEY;

      public String getKaptchaParameter() {
      return this.kaptchaParameter;
      }

      public void setKaptchaParameter(final String kaptchaParameter) {
      this.kaptchaParameter = kaptchaParameter;
      }


      @Override

      public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) {
      log.error("attemptAuthentication: [{}]", "Override attemptAuthentication...................................");
      // 1. 判断请求方式是否是 POST
      if (!request.getMethod().equals("POST")) {
      throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
      }
      // TODO: 先进行验证码校验
      // 获取请求中的验证码
      String parameter = request.getParameter(getKaptchaParameter());
      String code = (String) request.getSession().getAttribute("kaptcha");
      // 用户输入的验证码和 session 作用域中的都不能为空
      if (!ObjectUtils.isEmpty(parameter) && !ObjectUtils.isEmpty(code) && parameter.equalsIgnoreCase(code)) {
      return super.attemptAuthentication(request, response);
      }
      // 没有通过则执行自定义异常
      throw new KaptchaNotMatchException("验证码不匹配!");

      }
      }

    • 自定义验证码异常

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      package com.example.exception;


      import org.springframework.security.core.AuthenticationException;

      public class KaptchaNotMatchException extends AuthenticationException {
      public KaptchaNotMatchException(String msg, Throwable cause) {
      super(msg, cause);
      }

      public KaptchaNotMatchException(String msg) {
      super(msg);
      }
      }

    • Security 配置类

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      package com.example.config;

      import com.example.filter.LoginFilter;
      import com.example.service.MyUserDetailsService;
      import com.fasterxml.jackson.databind.ObjectMapper;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.context.annotation.Bean;
      import org.springframework.context.annotation.Configuration;
      import org.springframework.http.HttpStatus;
      import org.springframework.http.MediaType;
      import org.springframework.security.authentication.AuthenticationManager;
      import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
      import org.springframework.security.config.annotation.web.builders.HttpSecurity;
      import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
      import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

      import java.util.HashMap;
      import java.util.Map;

      @Configuration
      public class SecurityConfigure extends WebSecurityConfigurerAdapter {
      // 使用数据库中的用户认证
      private final MyUserDetailsService myUserDetailsService;

      @Autowired
      public SecurityConfigure(final MyUserDetailsService myUserDetailsService) {
      this.myUserDetailsService = myUserDetailsService;
      }


      @Override
      protected void configure(AuthenticationManagerBuilder auth) throws Exception {
      auth.userDetailsService(myUserDetailsService);
      }

      @Override
      @Bean
      public AuthenticationManager authenticationManagerBean() throws Exception {
      return super.authenticationManagerBean();
      }

      // 自定义 filter 交给工厂管理
      @Bean
      public LoginFilter loginFilter() throws Exception {
      LoginFilter loginFilter = new LoginFilter();
      // 接受指定的 json 用户名 key
      loginFilter.setUsernameParameter("username");
      // 接受指定的 json 密码 key
      loginFilter.setPasswordParameter("password");
      // 接受指定的 json 验证码 key
      loginFilter.setKaptchaParameter("code");
      // 注入 authdicationManager
      loginFilter.setAuthenticationManager(authenticationManagerBean());
      // 认证成功后
      loginFilter.setAuthenticationSuccessHandler((req, resp, auth) -> {
      Map<String, Object> result = new HashMap<>();
      result.put("msg", "登录成功");
      // 状态码: 200
      resp.setStatus(HttpStatus.OK.value());
      result.put("authentication", auth);
      resp.setContentType("application/json;charset=UTF-8");
      String json = new ObjectMapper().writeValueAsString(result);
      resp.getWriter().println(json);
      });
      // 认证失败后
      loginFilter.setAuthenticationFailureHandler((req, resp, ex) -> {
      Map<String, Object> result = new HashMap<>();
      result.put("msg", "登录失败: " + ex.getMessage());
      // 服务器内部错误 状态码: 500
      resp.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
      resp.setContentType("application/json;charset=UTF-8");
      String json = new ObjectMapper().writeValueAsString(result);
      resp.getWriter().println(json);
      });
      return loginFilter;
      }

      // 配置安全
      @Override
      protected void configure(HttpSecurity http) throws Exception {
      http.authorizeRequests()
      // 放行资源
      .antMatchers("/captcha/**", "/index", "/login","/code").permitAll()
      // 所有请求都需要认证
      .anyRequest().authenticated().and().formLogin()
      // 使用自定义登录页面
      .loginPage("/login")
      // 退出
      .and()


      // 认证异常的处理
      .exceptionHandling()
      .authenticationEntryPoint((req, resp, ex) -> {
      Map<String, Object> result = new HashMap<>();
      result.put("msg", "请认证之后再去处理....");
      result.put("status", HttpStatus.UNAUTHORIZED.value());
      resp.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
      String json = new ObjectMapper().writeValueAsString(result);
      resp.getWriter().println(json);
      })


      .and()
      .logout().logoutUrl("/logout")
      // 退出成功后
      .logoutSuccessHandler((req, resp, auth) -> {
      Map<String, Object> result = new HashMap<>();
      result.put("msg", "注销成功");
      result.put("authentication", auth.getPrincipal());
      resp.setContentType("application/json;charset=UTF-8");
      resp.setStatus(HttpStatus.OK.value());
      String json = new ObjectMapper().writeValueAsString(result);
      resp.getWriter().println(json);
      })
      // 禁用跨站
      .and().csrf().disable();
      // at: 用在某个 filter 替换过滤器链中哪个 filter
      // before: 放在过滤器链中那个 filter 之前
      // after: 放在过滤器链中那个 filter 之后
      http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class);
      }
      }

    • 前端页面

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      <!DOCTYPE html>
      <html lang="en" xmlns:th="http://www.thymeleaf.org">

      <head>
      <meta charset="UTF-8">
      <title>登录</title>
      </head>
      <body>
      <form th:action="@{/login}" method="post">
      <p>
      用户名: <input type="text" name="username"/>
      </p>
      <p>
      密码: <input type="text" name="password"/>
      </p>
      <p>
      验证码: <input type="text" name="code"/><img th:src="@{/code}">
      </p>
      <input type="submit" value="登录">
      </form>

      </body>
      </html>
      输入错误的验证码
  • 前后端分离的验证码认证

    • 生成验证码控制器

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      package com.example.controller;

      import com.google.code.kaptcha.Producer;
      import org.apache.tomcat.util.codec.binary.Base64;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.util.FastByteArrayOutputStream;
      import org.springframework.web.bind.annotation.GetMapping;
      import org.springframework.web.bind.annotation.RestController;

      import javax.imageio.ImageIO;
      import javax.servlet.http.HttpSession;
      import java.awt.image.BufferedImage;
      import java.io.IOException;

      @RestController
      public class SplitCaptchaController {
      private final Producer producer;

      @Autowired
      public SplitCaptchaController(final Producer producer) {
      this.producer = producer;
      }
      @GetMapping("/split/code")
      public String getVerifyCode(HttpSession session) throws IOException {
      // 生成验证码
      String text = producer.createText();
      // 存储到 session/redis
      session.setAttribute("captcha", text);
      // 生成图片
      BufferedImage image = producer.createImage(text);
      FastByteArrayOutputStream fos = new FastByteArrayOutputStream();
      ImageIO.write(image, "jpg", fos);
      // 返回 base64
      return Base64.encodeBase64String(fos.toByteArray());
      }
      }

    • base64 转图片前缀

      1
      data:image/jpg;base64,
      base64
    • 更换 JSON 获取

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      package com.example.filter;

      import com.example.exception.KaptchaNotMatchException;
      import com.fasterxml.jackson.databind.ObjectMapper;
      import lombok.extern.slf4j.Slf4j;
      import org.springframework.http.MediaType;
      import org.springframework.security.authentication.AuthenticationServiceException;
      import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
      import org.springframework.security.core.Authentication;
      import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
      import org.springframework.util.ObjectUtils;

      import javax.servlet.http.HttpServletRequest;
      import javax.servlet.http.HttpServletResponse;
      import java.io.IOException;
      import java.util.Map;

      /**
      * 自定义前后端认证
      */
      @Slf4j
      public class LoginFilter extends UsernamePasswordAuthenticationFilter {
      private static final String FORM_KAPTCHA_KEY = "code";
      private String kaptchaParameter = FORM_KAPTCHA_KEY;

      public String getKaptchaParameter() {
      return this.kaptchaParameter;
      }

      public void setKaptchaParameter(final String kaptchaParameter) {
      this.kaptchaParameter = kaptchaParameter;
      }


      @Override

      public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) {
      log.error("attemptAuthentication: [{}]", "Override attemptAuthentication...................................");
      // 1. 判断请求方式是否是 POST
      if (!request.getMethod().equals("POST")) {
      throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
      }
      // 2. 判断 数据是否是 JSON 格式
      if (request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE)) {
      try {
      // 将请求体中的数据进行反序列化
      Map<String, String> userInfo = new ObjectMapper().readValue(request.getInputStream(), Map.class);
      // TODO: 先进行验证码校验
      // 获取请求中的验证码
      String parameter = userInfo.get(getKaptchaParameter());
      String username = userInfo.get(getUsernameParameter());
      String password = userInfo.get(getPasswordParameter());
      // 获取 session 中的验证码
      String code = (String) request.getSession().getAttribute("kaptcha");

      log.error("username: [{}]", username);
      log.error("password: [{}]", password);
      log.error("code: [{}]", code);

      // 用户输入的验证码和 session 作用域中的都不能为空
      if (!ObjectUtils.isEmpty(parameter) && !ObjectUtils.isEmpty(code) && parameter.equalsIgnoreCase(code)) {
      UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
      this.setDetails(request, authRequest);
      return super.attemptAuthentication(request, response);
      }
      } catch (IOException e) {
      throw new RuntimeException(e);
      }
      throw new KaptchaNotMatchException("验证码不匹配");
      }
      return super.attemptAuthentication(request, response);
      }
      }

密码加密

  • 认识PasswordEncoder

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public interface PasswordEncoder {
    String encode(CharSequence rawPassword);

    boolean matches(CharSequence rawPassword, String encodedPassword);

    default boolean upgradeEncoding(String encodedPassword) {
    return false;
    }
    }

    • encode: 用来进行铭文加密的
    • matches: 用来比较密码的方法
    • upgradeEncoding: 用来给密码进行升级的方法
  • 所有未过期的实现类

    所有未过期的实现类
  • 密码更新

    • 实现Service

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      package com.example.service;

      import com.example.entity.Role;
      import com.example.entity.User;
      import com.example.mapper.UserMapper;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.security.core.userdetails.UserDetails;
      import org.springframework.security.core.userdetails.UserDetailsPasswordService;
      import org.springframework.security.core.userdetails.UserDetailsService;
      import org.springframework.security.core.userdetails.UsernameNotFoundException;
      import org.springframework.stereotype.Service;
      import org.springframework.util.ObjectUtils;

      import java.util.List;

      @Service
      public class MyUserDetailsService implements UserDetailsService, UserDetailsPasswordService {

      private final UserMapper userMapper;

      @Autowired
      public MyUserDetailsService(final UserMapper userMapper) {
      this.userMapper = userMapper;
      }

      @Override
      public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
      // 根据用户名查询用户
      User user = userMapper.findUserByUserName(username);
      // 如果用户为空
      if (ObjectUtils.isEmpty(user)) throw new UsernameNotFoundException("用户名错误!");
      // 查询权限信息
      List<Role> roles = userMapper.getRoleByUid(user.getId());
      user.setRoles(roles);
      return user;
      }

      // 实现密码更新
      @Override
      public UserDetails updatePassword(UserDetails user, String newPassword) {
      Integer updatePassword = userMapper.updatePassword(user.getUsername(), newPassword);
      if (updatePassword == 1) {
      ((User) user).setPassword(newPassword);
      }
      return user;
      }
      }

    • UserMapper

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      <?xml version="1.0" encoding="UTF-8" ?>
      <!DOCTYPE mapper
      PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
      "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
      <mapper namespace="com.example.mapper.UserMapper">
      <!-- User findUserByUserName(String username); -->
      <select id="findUserByUserName" resultType="user">
      select *
      from user
      where username = #{username}
      </select>

      <!-- List<Role> getRoleByUid(Integer uid); -->
      <select id="getRoleByUid" resultType="role">
      select r.id, r.name, r.name_zh
      from role r,
      user_role ur
      where r.id = ur.uid
      and ur.uid = #{uid}
      </select>

      <!-- Integer updatePassword(@Param("username") String username,@Param("password") String password);-->
      <update id="updatePassword">
      update `user`
      set password = #{password}
      where username = #{username}
      </update>
      </mapper>

    • 更新密码是在原密码上直接更新用户密码的加密方式

      {noop}(明文) => {bcrypt}加密方式

RemaemberMe

  • 配置类中添加

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
    .anyRequest().authenticated()

    .and().formLogin()
    // 记住我
    .and().rememberMe()

    .and().csrf().disable();
    }
  • 自定义登录认证

    开启后 根据源码分析,在自定义页面上实现如下即可
    在这里插入图片描述
  • 原理分析

    勾选后,传递参数remember-me: on
    RememberMeAuthenticationFilter 源码分析
    1
    2
    // RememberMeAuthenticationFilter 中的核心
    Authentication rememberMeAuth = this.rememberMeServices.autoLogin(request, response);
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    // 核心 autologin(会话超时后(一段时间过期))触发autologin 
    public final Authentication autoLogin(HttpServletRequest request, HttpServletResponse response) {
    String rememberMeCookie = this.extractRememberMeCookie(request);
    if (rememberMeCookie == null) {
    return null;
    } else {
    this.logger.debug("Remember-me cookie detected");
    if (rememberMeCookie.length() == 0) {
    this.logger.debug("Cookie was empty");
    this.cancelCookie(request, response);
    return null;
    } else {
    try {
    String[] cookieTokens = this.decodeCookie(rememberMeCookie);
    UserDetails user = this.processAutoLoginCookie(cookieTokens, request, response);
    this.userDetailsChecker.check(user);
    this.logger.debug("Remember-me cookie accepted");
    return this.createSuccessfulAuthentication(request, user);
    } catch (CookieTheftException var6) {
    this.cancelCookie(request, response);
    throw var6;
    } catch (UsernameNotFoundException var7) {
    this.logger.debug("Remember-me login was valid but corresponding user not found.", var7);
    } catch (InvalidCookieException var8) {
    this.logger.debug("Invalid remember-me cookie: " + var8.getMessage());
    } catch (AccountStatusException var9) {
    this.logger.debug("Invalid UserDetails: " + var9.getMessage());
    } catch (RememberMeAuthenticationException var10) {
    this.logger.debug(var10.getMessage());
    }

    this.cancelCookie(request, response);
    return null;
    }
    }
    }
    cookie: remember-me
  • 总结

    当用户通过用户名/密码的形式登录成功后,系统会根据用户的用户名、密码以及令牌的过期时间计算出一个签名,这个签名使用MD5 消息摘要算法生成,是不可逆的。然后再将用户名,令牌过期时间以及签名拼接成一个字符串,中间用: 隔开,对拼接好的字符串进行Base64 编码,然后再将编码后的结果返回到前端,也就是我们在浏览器中看到的令牌。当会话过期后,访问系统资源时会自动携带cookie 中的令牌,服务端拿到cookie 中的令牌后,先进行Base64 解码,如果没有过期,则根据令牌中的用户名和查询出用户信息,接着在计算出一个签名和令牌中的签名进行对比,如果一致,表示会牌是合法令牌,自动登录成功,否则自动登录失败

  • 原理图总结

    总结
  • 配置持久化令牌

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    // 前提是整合: mybatis 
    package com.example.config;

    import com.example.service.MyUserDetailsService;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.authentication.AuthenticationManager;
    import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
    import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;

    import javax.sql.DataSource;

    @Slf4j
    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    private final DataSource dataSource;
    private final MyUserDetailsService myUserDetailsService;

    @Autowired
    public SecurityConfig(DataSource dataSource, final MyUserDetailsService myUserDetailsService) {
    this.dataSource = dataSource;
    this.myUserDetailsService = myUserDetailsService;
    }

    // 自定义 AuthenticationManager 推荐 并没有在工厂在暴露出来
    @Override
    protected void configure(AuthenticationManagerBuilder builder) throws Exception {
    log.error("自定义 AuthenticationManager: " + builder);
    builder.userDetailsService(myUserDetailsService);
    }

    // 作用: 用来将自定义 AuthenticationManager 在工厂中进行暴露 可以在任意位置注入
    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
    return super.authenticationManagerBean();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
    .anyRequest().authenticated().and().formLogin()

    // 记住我
    .and()
    .rememberMe()
    // rememberMe 持久化
    .tokenRepository(persistentTokenRepository())


    .and().csrf().disable();
    }

    @Bean
    public PersistentTokenRepository persistentTokenRepository() {
    JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
    jdbcTokenRepository.setDataSource(dataSource);
    // 第一次启动时创建表结构(true) 第二次: false
    jdbcTokenRepository.setCreateTableOnStartup(true);
    return jdbcTokenRepository;
    }
    }

    • 错误

      错误原因:jdbcTokenRepository.setCreateTableOnStartup(true); 第二次启动时数据库中已经存在表结构
    • 创建的表

      rememberMe实现持久化

自定义记住我(前后端分离实现)

  • 传统WEB

    • 配置中开启记住我

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      @Override
      protected void configure(HttpSecurity http) throws Exception {
      http.authorizeRequests()
      .antMatchers("/login").permitAll()
      .anyRequest().authenticated().and()
      .formLogin()
      .loginPage("/login")
      .loginProcessingUrl("/login")
      // 登录成功后的跳转
      .defaultSuccessUrl("/test", true)
      .and()
      // 记住我
      .rememberMe()
      // remember 的持久化
      .tokenRepository(persistentTokenRepository())
      .and().csrf().disable();
      }
    • checkbox 的值

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      // 源码
      protected boolean rememberMeRequested(HttpServletRequest request, String parameter) {
      if (this.alwaysRemember) {
      return true;
      } else {
      String paramValue = request.getParameter(parameter);
      if (paramValue == null || !paramValue.equalsIgnoreCase("true") && !paramValue.equalsIgnoreCase("on") && !paramValue.equalsIgnoreCase("yes") && !paramValue.equals("1")) {
      this.logger.debug(LogMessage.format("Did not send remember-me cookie (principal did not set parameter '%s')", parameter));
      return false;
      } else {
      return true;
      }
      }
      }
    • 页面定义

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      <!DOCTYPE html>
      <html lang="en" xmlns:th="http://www.thymeleaf.org">
      <head>
      <meta charset="UTF-8">
      <title>登录</title>
      </head>
      <body>
      <form th:action="@{/login}" method="post">
      <p>
      用户名: <input type="text" name="username"/>
      </p>
      <p>
      密码: <input type="text" name="password"/>
      </p>
      <p>
      记住我: <input name="remember-me" type="checkbox" value="true"/>
      </p>
      <input type="submit" value="登录">
      </form>
      </body>
      </html>
    • 点击记住我登录,生成cookie

      cookie
  • 前后端分离

    • 创建一个前后端分离的项目

    • rememberMeRequested => AbstractRememberMeServices(类中)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      protected boolean rememberMeRequested(HttpServletRequest request, String parameter) {
      if (this.alwaysRemember) {
      return true;
      } else {
      String paramValue = request.getParameter(parameter);
      if (paramValue == null || !paramValue.equalsIgnoreCase("true") && !paramValue.equalsIgnoreCase("on") && !paramValue.equalsIgnoreCase("yes") && !paramValue.equals("1")) {
      this.logger.debug(LogMessage.format("Did not send remember-me cookie (principal did not set parameter '%s')", parameter));
      return false;
      } else {
      return true;
      }
      }
      }
    • security 配置类

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      package com.example.config;

      import com.example.filter.LoginFilter;
      import com.fasterxml.jackson.databind.ObjectMapper;
      import org.springframework.context.annotation.Bean;
      import org.springframework.context.annotation.Configuration;
      import org.springframework.http.HttpStatus;
      import org.springframework.http.MediaType;
      import org.springframework.security.authentication.AuthenticationManager;
      import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
      import org.springframework.security.config.annotation.web.builders.HttpSecurity;
      import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
      import org.springframework.security.core.userdetails.User;
      import org.springframework.security.core.userdetails.UserDetailsService;
      import org.springframework.security.provisioning.InMemoryUserDetailsManager;
      import org.springframework.security.web.authentication.RememberMeServices;
      import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
      import org.springframework.security.web.authentication.rememberme.InMemoryTokenRepositoryImpl;

      import java.util.HashMap;
      import java.util.Map;
      import java.util.UUID;

      @Configuration
      public class SecurityConfigure extends WebSecurityConfigurerAdapter {
      // 使用内存中的用户认证
      @Bean
      public UserDetailsService userDetailsService() {
      InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
      inMemoryUserDetailsManager.createUser(User.withUsername("root").password("{noop}123").roles("admin").build());
      return inMemoryUserDetailsManager;
      }

      @Override
      protected void configure(AuthenticationManagerBuilder auth) throws Exception {
      auth.userDetailsService(userDetailsService());
      }

      @Override
      @Bean
      public AuthenticationManager authenticationManagerBean() throws Exception {
      return super.authenticationManagerBean();
      }

      // 自定义 filter 交给工厂管理
      @Bean
      public LoginFilter loginFilter() throws Exception {
      LoginFilter loginFilter = new LoginFilter();
      // 指定认证的 url
      loginFilter.setFilterProcessesUrl("/doLogin");
      // 接受指定的 json 用户名 key
      loginFilter.setUsernameParameter("username");
      // 接受指定的 json 密码 key
      loginFilter.setPasswordParameter("password");
      // 注入 authenticationManager
      loginFilter.setAuthenticationManager(authenticationManagerBean());
      // TODO: 设置认证成功后时使用自定义 rememberMeServices
      loginFilter.setRememberMeServices(rememberMeServices());
      // 认证成功后
      loginFilter.setAuthenticationSuccessHandler((req, resp, auth) -> {
      Map<String, Object> result = new HashMap<>();
      result.put("msg", "登录成功");
      // 状态码: 200
      resp.setStatus(HttpStatus.OK.value());
      result.put("authentication", auth);
      resp.setContentType("application/json;charset=UTF-8");
      String json = new ObjectMapper().writeValueAsString(result);
      resp.getWriter().println(json);
      });
      // 认证失败后
      loginFilter.setAuthenticationFailureHandler((req, resp, ex) -> {
      Map<String, Object> result = new HashMap<>();
      result.put("msg", "登录失败: " + ex.getMessage());
      // 服务器内部错误 状态码: 500
      resp.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
      resp.setContentType("application/json;charset=UTF-8");
      String json = new ObjectMapper().writeValueAsString(result);
      resp.getWriter().println(json);
      });
      return loginFilter;
      }

      // 配置安全
      @Override
      protected void configure(HttpSecurity http) throws Exception {
      http.authorizeRequests()
      // 所有请求都需要认证
      .anyRequest().authenticated().and().formLogin()
      // TODO: 1. 认证成功保存记住我 cookie 到客户端 2. 只有 cookie 写入到客户端成功才能实现自动登录功能
      .and().rememberMe()
      // 设置自动登录使用那个 rememberMeServices
      .rememberMeServices(rememberMeServices())
      // 退出
      .and()
      .exceptionHandling()
      .authenticationEntryPoint((req, resp, ex) -> {
      Map<String, Object> result = new HashMap<>();
      result.put("msg", "请认证之后再去处理....");
      result.put("status", HttpStatus.UNAUTHORIZED.value());
      resp.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
      String json = new ObjectMapper().writeValueAsString(result);
      resp.getWriter().println(json);
      })
      .and()
      .logout().logoutUrl("/logout")
      // 退出成功后
      .logoutSuccessHandler((req, resp, auth) -> {
      Map<String, Object> result = new HashMap<>();
      result.put("msg", "注销成功");
      result.put("authentication", auth.getPrincipal());
      resp.setContentType("application/json;charset=UTF-8");
      resp.setStatus(HttpStatus.OK.value());
      String json = new ObjectMapper().writeValueAsString(result);
      resp.getWriter().println(json);
      })
      // 禁用跨站
      .and().csrf().disable();
      // at: 用在某个 filter 替换过滤器链中哪个 filter
      // before: 放在过滤器链中那个 filter 之前
      // after: 放在过滤器链中那个 filter 之后
      http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class);
      }

      @Bean
      public RememberMeServices rememberMeServices() {
      return new MyPersistentTokenBasedRememberMeServices(UUID.randomUUID().toString(), userDetailsService(), new InMemoryTokenRepositoryImpl());
      }
      }

    • 自定义过滤器

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      package com.example.filter;

      import com.example.config.MyPersistentTokenBasedRememberMeServices;
      import com.fasterxml.jackson.databind.ObjectMapper;
      import lombok.extern.slf4j.Slf4j;
      import org.springframework.http.MediaType;
      import org.springframework.security.authentication.AuthenticationServiceException;
      import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
      import org.springframework.security.core.Authentication;
      import org.springframework.security.web.authentication.RememberMeServices;
      import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
      import org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices;
      import org.springframework.util.ObjectUtils;

      import javax.servlet.http.HttpServletRequest;
      import javax.servlet.http.HttpServletResponse;
      import java.io.IOException;
      import java.util.Map;
      import java.util.UUID;

      /**
      * 自定义前后端认证
      */
      @Slf4j
      public class LoginFilter extends UsernamePasswordAuthenticationFilter {
      @Override
      public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) {
      log.error("attemptAuthentication: [{}]", "Override attemptAuthentication...................................");
      // 1. 判断请求方式是否是 POST
      if (!request.getMethod().equals("POST")) {
      throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
      }
      // 2. 判断 数据是否是 JSON 格式
      if (request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE)) {
      try {
      // 将请求体中的数据进行反序列化
      Map<String, String> userInfo = new ObjectMapper().readValue(request.getInputStream(), Map.class);
      String username = userInfo.get(getUsernameParameter());
      String password = userInfo.get(getPasswordParameter());
      // 记住我 AbstractRememberMeServices.DEFAULT_PARAMETER = remember-me
      String rememberValue = userInfo.get(AbstractRememberMeServices.DEFAULT_PARAMETER);
      if (!ObjectUtils.isEmpty(rememberValue)) {
      request.setAttribute(AbstractRememberMeServices.DEFAULT_PARAMETER, rememberValue);
      }
      log.error("username: [{}]", username);
      log.error("password: [{}]", password);
      log.error("remember: [{}]", rememberValue);
      UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
      this.setDetails(request, authRequest);
      return this.getAuthenticationManager().authenticate(authRequest);
      } catch (IOException e) {
      throw new RuntimeException(e);
      }
      }
      // 如果不是 JSON 格式数据,则调用传统方式进行认证
      return super.attemptAuthentication(request, response);
      }
      }

    • PersistentTokenBasedRememberMeServices 类集成重写

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      package com.example.config;

      import lombok.extern.slf4j.Slf4j;
      import org.springframework.core.log.LogMessage;
      import org.springframework.security.core.userdetails.UserDetailsService;
      import org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices;
      import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
      import org.springframework.stereotype.Component;

      import javax.servlet.http.HttpServletRequest;

      @Slf4j
      public class MyPersistentTokenBasedRememberMeServices extends PersistentTokenBasedRememberMeServices {

      public MyPersistentTokenBasedRememberMeServices(String key, UserDetailsService userDetailsService, PersistentTokenRepository tokenRepository) {
      super(key, userDetailsService, tokenRepository);
      }

      /**
      * 自定义获取 remember-me 方式
      *
      * @param request
      * @param parameter
      * @return
      */
      @Override
      protected boolean rememberMeRequested(HttpServletRequest request, String parameter) {
      log.info("parameter: [{}]", parameter);
      String paramValue = request.getAttribute(parameter).toString();
      if (paramValue == null || !paramValue.equalsIgnoreCase("true") && !paramValue.equalsIgnoreCase("on") && !paramValue.equalsIgnoreCase("yes") && !paramValue.equals("1")) {
      return false;
      } else {
      return true;
      }
      }
      }

    • 源文件

      https://gitee.com/coder-itl/spring-security/tree/remember-me

    • 测试

      配置session 过期时间 remember-me
    • 必须传递remember-me?

      如何优雅处理remember-me

会话

简介

当浏览器调用接口登录成功后,服务端会和浏览器之间建立一个会话(session)浏览器在每次发送请求时都会携带一个SessionId,服务端则根据这个SessionId 来判断用户身份。当浏览器关闭后,服务端的Session 并不会自动销毁,需要开发者手动在服务端调用Session 销毁方法,或者等Session 过期时间到了自动销毁。在Spring Security 中,HttpSession 相关的功能由SessionManagementFilter Session AutheaticationStrateey 接口来处理,SessionManagomentFiter 过滤器将session 相关操作委托给SessionAuthenticationStrategy 接口去完成。

会话并发管理
  • 简介

    会话并发管理就是指在当前系统中,同一个用户可以同时创建多少个会话,如果一个设备对应一个会话,那么也可以简单理解为同一个用户可以同时在多少台设备上进行登录。默认情况下,同一用户在多少台设备上登录并没有限制,不过开发者可以在spring security 中对此进行配置

  • 会话配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    package com.example.config;

    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.web.session.HttpSessionEventPublisher;

    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    http.authorizeHttpRequests()
    .anyRequest().authenticated()
    .and().formLogin()
    .and().csrf().disable()
    // 开启会话管理
    .sessionManagement()
    // 允许会话最大并发只能一个客户端 设置会话的并发数
    .maximumSessions(1)
    ;
    }

    @Bean
    public HttpSessionEventPublisher httpSessionEventPublisher() {
    return new HttpSessionEventPublisher();
    }
    }

    • 访问测试

      第二个访问会导致第一个访问失效
    • 说明

      1. sessionManagement() 用来开启会话管理,maximumSessions 指定会话的并发数
      2. HttpSessionEventPublisher 提供一个Http SessionEventPublishor 实例,SpringSecurity 中通过一个Map 集合来维护当前的HttpSession 记录,进而实现会话的并发管理。当用户登录成功时,就像集合中添加一条Http Session 记录,当会话销毁时,就从集合中移除一条HttpSession 记录,HttpSessionEventPublisher 实现了Fttp SessionListener 接口,可以监听到HttpSession 的创建和销毁事件,并将Fltp Session 的创建/销毁事件发布出去,这样,当有HttpSession 销毁时,SpringSecurity 就可以感知到该事件了。
传统WEB开发处理失效
  • 配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    package com.example.config;

    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.web.session.HttpSessionEventPublisher;

    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    http.authorizeHttpRequests()
    .anyRequest().authenticated()
    .and().formLogin()
    .and().csrf().disable()
    // 开启会话管理
    .sessionManagement()
    // 允许会话最大并发只能一个客户端 设置会话的并发数
    .maximumSessions(1)


    // TODO: 传统 web 失效处理 当用户被挤下线之后跳转路径
    .expiredUrl("/login")
    ;
    }

    @Bean
    public HttpSessionEventPublisher httpSessionEventPublisher() {
    return new HttpSessionEventPublisher();
    }
    }

前后端分离失效处理
  • 配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    package com.example.config;

    import com.fasterxml.jackson.databind.ObjectMapper;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.web.session.HttpSessionEventPublisher;

    import javax.servlet.http.HttpServletResponse;
    import java.util.HashMap;
    import java.util.Map;

    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    http.authorizeHttpRequests()
    .anyRequest().authenticated()
    .and().formLogin()
    .and().csrf().disable()
    // 开启会话管理
    .sessionManagement()
    // 允许会话最大并发只能一个客户端 设置会话的并发数
    .maximumSessions(1)
    // TODO: 前后端分离
    .expiredSessionStrategy(event -> {
    HttpServletResponse response = event.getResponse();
    response.setContentType("application/json;charset=UTF-8");
    Map<String, Object> result = new HashMap<>();
    result.put("status", 500);
    result.put("msg", "当前会话已经失效,请重新登录!");
    String json = new ObjectMapper().writeValueAsString(result);
    response.getWriter().println(json);
    response.flushBuffer();
    })
    ;
    }

    @Bean
    public HttpSessionEventPublisher httpSessionEventPublisher() {
    return new HttpSessionEventPublisher();
    }
    }

  • 访问测试

    前后分离会话失效处理
禁止再次登录
  • 即: 只允许在一台设备登录,只有登录设备主动退出后,其他客户端才允许登录

  • 配置

    1
    2
    3
    4
    5
    6
    // 开启会话管理
    .sessionManagement()
    // 允许会话最大并发只能一个客户端 设置会话的并发数
    .maximumSessions(1)
    // TODO: 登录之后禁止再次登录
    .maxSessionsPreventsLogin(true)
    只允许一台客户端登录
集群会话共享解决方案
  • 配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    package com.example.config;

    import com.fasterxml.jackson.databind.ObjectMapper;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.web.session.HttpSessionEventPublisher;
    import org.springframework.session.FindByIndexNameSessionRepository;
    import org.springframework.session.security.SpringSessionBackedSessionRegistry;

    import javax.servlet.http.HttpServletResponse;
    import java.util.HashMap;
    import java.util.Map;

    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    private final FindByIndexNameSessionRepository findByIndexNameSessionRepository;

    @Autowired
    public SecurityConfig(FindByIndexNameSessionRepository findByIndexNameSessionRepository) {
    this.findByIndexNameSessionRepository = findByIndexNameSessionRepository;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
    http.authorizeHttpRequests()
    .anyRequest().authenticated()
    .and().formLogin()
    .and().logout()
    .and().csrf().disable()
    // 开启会话管理
    .sessionManagement()
    // 允许会话最大并发只能一个客户端 设置会话的并发数
    .maximumSessions(1)
    // 登录之后禁止再次登录
    .maxSessionsPreventsLogin(true)
    // 将 session 交给谁管理
    .sessionRegistry(sessionRegistry())
    // TODO: 前后端分离
    .expiredSessionStrategy(event -> {
    HttpServletResponse response = event.getResponse();
    response.setContentType("application/json;charset=UTF-8");
    Map<String, Object> result = new HashMap<>();
    result.put("status", 500);
    result.put("msg", "当前会话已经失效,请重新登录!");
    String json = new ObjectMapper().writeValueAsString(result);
    response.getWriter().println(json);
    response.flushBuffer();
    })
    ;
    }
    // 创建 session 同步到 redis 中方案
    @Bean
    public SpringSessionBackedSessionRegistry sessionRegistry() {
    return new SpringSessionBackedSessionRegistry(findByIndexNameSessionRepository);
    }
    }

  • 创建项目集群

    • 拷贝并修改端口

      拷贝(①) 添加VM
      在这里插入图片描述
    • 修改端口

      ③ 修改端口-Dserver.port=8081
  • 连接redis 客户端,查看访问之后生成的信息

    不同客户端访问测试(注意端口) 集群会话记录

CSRF 漏洞保护

简介
  • CSRF(Cross-Site Request Forgery 跨站请求伪造),也可以称为一键式攻击(one-click-attack),通常缩写为CSRF/XSRF
  • CSRF 攻击是一种挟持用户在当前已登录的浏览器上发送恶意请求的攻击方法。相对于XSS 利用用户对指定网站的信任,CSRF 则是利用网站对用户网页浏览器的信任。简单来说,CSRF 是攻击者通过一些技术手段欺骗用户的浏览器,去访问一个用户层级认证过的网站并执行恶意请求,例如发送右键、发送消息,甚至财产操作(如转账和购买商品)。由于客户端(浏览器)已经在该网站上认证过,所以该网站会认为是真正用户在操作而执行的请求(实际上这个并非的本意)
开启 CSRF 防御
  • 配置

    1
    .csrf();
  • 查看网页源码

    携带CSRF 令牌
传统WEB开发中使用CSRF
  • 传统web 只需要在配置类中开启csrf 即可

  • 开启CSRF 防御后会自动在提交的表单中加入如下代码,需要在开启之后手动加入如下代码,并随着请求提交。获取服务端令牌方式如下

    1
    <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
前后分离使用 CSRF
  • 前后端分离开发时,只需要将生成的csrf 放入到cookie 中,并在请求时获取cookie 中令牌信息进行提交即可。

  • 访问前后端分离下的login,使其产生cookie

    生成XSRF-TOKEN cookie 认证未通过
  • 配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    package com.example.config;

    @Configuration
    public class SecurityConfigure extends WebSecurityConfigurerAdapter {
    // 使用内存中的用户认证
    @Bean
    public UserDetailsService userDetailsService() {
    InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
    inMemoryUserDetailsManager.createUser(User.withUsername("root").password("{noop}root").roles("admin").build());
    return inMemoryUserDetailsManager;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userDetailsService());
    }

    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
    return super.authenticationManagerBean();
    }

    // 自定义 filter 交给工厂管理
    @Bean
    public LoginFilter loginFilter() throws Exception {
    LoginFilter loginFilter = new LoginFilter();
    // 指定认证的 url
    loginFilter.setFilterProcessesUrl("/login");
    // 接受指定的 json 用户名 key
    loginFilter.setUsernameParameter("username");
    // 接受指定的 json 密码 key
    loginFilter.setPasswordParameter("password");
    // 注入 authenticationManager
    loginFilter.setAuthenticationManager(authenticationManagerBean());
    // 认证成功后
    loginFilter.setAuthenticationSuccessHandler((req, resp, auth) -> {
    Map<String, Object> result = new HashMap<>();
    result.put("msg", "登录成功");
    // 状态码: 200
    resp.setStatus(HttpStatus.OK.value());
    result.put("authentication", auth);
    resp.setContentType("application/json;charset=UTF-8");
    String json = new ObjectMapper().writeValueAsString(result);
    resp.getWriter().println(json);
    });
    // 认证失败后
    loginFilter.setAuthenticationFailureHandler((req, resp, ex) -> {
    Map<String, Object> result = new HashMap<>();
    result.put("msg", "登录失败: " + ex.getMessage());
    // 服务器内部错误 状态码: 500
    resp.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
    resp.setContentType("application/json;charset=UTF-8");
    String json = new ObjectMapper().writeValueAsString(result);
    resp.getWriter().println(json);
    });
    return loginFilter;
    }

    // 配置安全
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
    // 所有请求都需要认证
    .anyRequest().authenticated().and().formLogin()
    // 退出
    .and()
    .exceptionHandling()
    .authenticationEntryPoint((req, resp, ex) -> {
    Map<String, Object> result = new HashMap<>();
    result.put("msg", "请认证之后再去处理....");
    result.put("status", HttpStatus.UNAUTHORIZED.value());
    resp.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
    String json = new ObjectMapper().writeValueAsString(result);
    resp.getWriter().println(json);
    })
    .and()
    .logout().logoutUrl("/logout")
    // 退出成功后
    .logoutSuccessHandler((req, resp, auth) -> {
    Map<String, Object> result = new HashMap<>();
    result.put("msg", "注销成功");
    result.put("authentication", auth.getPrincipal());
    resp.setContentType("application/json;charset=UTF-8");
    resp.setStatus(HttpStatus.OK.value());
    String json = new ObjectMapper().writeValueAsString(result);
    resp.getWriter().println(json);
    })
    // 开启跨站: 将令牌保存到 cookie 中并允许 cookie 被前端获取
    .and().csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
    // at: 用在某个 filter 替换过滤器链中哪个 filter
    // before: 放在过滤器链中那个 filter 之前
    // after: 放在过滤器链中那个 filter 之后
    http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class);
    }


    }

  • 控制器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    package com.example.controller;

    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RestController;

    @RestController
    public class IndexController {
    @PostMapping("/index")
    public String index() {
    return "index ok...";
    }
    }

  • 发送请求携带令牌即可

    • 请求参数中携带令牌

      1
      2
      key: _csrf
      value: "xxx"
    • 请求头中携带令牌

      1
      2
      // 前后端分离认证时,初次访问 /login,会生成一个 cookie,获取他得值,将该值以键 `X-XSRF-TOKEN',值为初次访问时生成的 cookie
      X-XSRF-TOKEN: value
      详细过程

      疑问: 那如何直接在第一次就可以登录成功!

      解决: 在vue 中使用请求拦截器之后使用vue-cookie 获取cookie 并设置到请求头

跨域

Spring跨域解决方案
  • SpringBoot 解决跨域方案

    • @CrossOrigin

    • 扩展

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      package com.example.config;

      import org.springframework.context.annotation.Configuration;
      import org.springframework.web.servlet.config.annotation.CorsRegistry;
      import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

      /**
      * 全局跨域处理
      */
      @Configuration
      public class WebMvcConfig implements WebMvcConfigurer {
      @Override
      public void addCorsMappings(CorsRegistry registry) {
      // 对那些请求进行跨域处理
      registry.addMapping("/**")
      .allowCredentials(false)
      .allowedHeaders("*")
      .allowedMethods("*")
      .allowedOrigins("*")
      .maxAge(3600);
      }
      }
  • CrosFilter: 是Spring Web 中提供的一个处理跨域的过滤器,开发者也可以通过该过滤器处理跨域

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    package com.example.config;

    import org.springframework.boot.web.servlet.FilterRegistrationBean;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.cors.CorsConfiguration;
    import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
    import org.springframework.web.filter.CorsFilter;

    import java.util.Arrays;

    @Configuration
    public class MyCrosFilter {
    @Bean
    FilterRegistrationBean<CorsFilter> cordFilter() {
    FilterRegistrationBean<CorsFilter> registrationBean = new FilterRegistrationBean<>();
    CorsConfiguration corsConfiguration = new CorsConfiguration();
    corsConfiguration.setAllowedHeaders(Arrays.asList("*"));
    corsConfiguration.setAllowedMethods(Arrays.asList("*"));
    corsConfiguration.setAllowedOrigins(Arrays.asList("*"));
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", corsConfiguration);
    registrationBean.setFilter(new CorsFilter(source));
    registrationBean.setOrder(-1);
    return registrationBean;
    }
    }

    注意: 引入Spring Security Spring/SpringBoot 中的跨域解决方案会失效。这句话读取后影响了后续的一些判断。

SpringSecurity跨域解决方案
  • 配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    package com.example.config;

    import org.springframework.web.cors.CorsConfiguration;
    import org.springframework.web.cors.CorsConfigurationSource;
    import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

    @Configuration
    public class SecurityConfigure extends WebSecurityConfigurerAdapter {

    // 配置安全
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    ...
    // 跨域 TODO: 这种跨域在后续出现失效,在初次出现这个文档时可以使用,但是当在后续重新写项目的时候采用此种配置时跨域失效了
    http.cors().configurationSource(configurationSource());
    ...
    }

    CorsConfigurationSource configurationSource() {
    CorsConfiguration corsConfiguration = new CorsConfiguration();
    corsConfiguration.setAllowedHeaders(Arrays.asList("*"));
    corsConfiguration.setAllowedMethods(Arrays.asList("*"));
    corsConfiguration.setAllowedOrigins(Arrays.asList("*"));
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", corsConfiguration);

    return source;
    }

    }

    解决跨域【第一次写文档时正常访问】

异常处理

  • 异常处理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    package com.example.config;


    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    public UserDetailsService userDetailsService() {
    InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
    inMemoryUserDetailsManager.createUser(User.withUsername("root").password("{noop}root").roles("ADMIN").build());
    inMemoryUserDetailsManager.createUser(User.withUsername("lisi").password("{noop}lisi").roles("USER").build());
    return inMemoryUserDetailsManager;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userDetailsService());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests().mvcMatchers("/hello").hasRole("ADMIN").anyRequest().authenticated().and().formLogin().and().csrf().disable();
    // 认证异常处理
    http.exceptionHandling().authenticationEntryPoint((req, resp, ex) -> {
    resp.setStatus(HttpStatus.UNAUTHORIZED.value());
    resp.setContentType("application/json;charset=UTF-8");
    resp.getWriter().write("尚未认证,请进行认证操作!");
    })
    // 授权异常处理
    .accessDeniedHandler((res, resp, ex) -> {
    resp.setStatus(HttpStatus.FORBIDDEN.value());
    resp.setContentType("application/json;charset=UTF-8");
    resp.getWriter().write("无权访问!");
    });

    }
    }

授权

权限管理策略
  • 基于过滤器的权限管理(FilterSecurityInterceptor)
    • 基于过滤器的权限管理主要是用来拦截HTTP 请求,拦截下来之后,根据HTTP 请求地址进行权限校验
  • 基于AOP 的权限管理(MethodSecurityInterceptor)
    • 基于AOP 权限管理主要是用来处理方法级别的权限问题。当需要调用某一个方法时,同过AOP 将操作拦截下来,然后判断用户受否具备相关的权限。
授权-URL-权限管理策略
  • 配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    package com.example.config;

    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.core.userdetails.User;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.provisioning.InMemoryUserDetailsManager;

    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    // 创建内存中的数据源
    public UserDetailsService userDetailsService() {
    InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
    // 角色权限
    inMemoryUserDetailsManager.createUser(User.withUsername("root").password("{noop}root").roles("ADMIN", "USER").build());
    inMemoryUserDetailsManager.createUser(User.withUsername("lisi").password("{noop}lisi").roles("USER").build());
    // 权限
    inMemoryUserDetailsManager.createUser(User.withUsername("wangwu").password("{noop}wangwu").authorities("READ_INFO").build());
    return inMemoryUserDetailsManager;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userDetailsService());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
    // 拥有的角色
    .mvcMatchers("/admin").hasRole("ADMIN")
    .mvcMatchers("/user").hasRole("USER")
    // 拥有权限
    .mvcMatchers("/info").hasAuthority("READ_INFO")
    .anyRequest().authenticated().and().formLogin().and().csrf().disable();
    }
    }

    // mvcMatchers() 匹配多种: /admin /admin/ /admin.html ...
    // antMatchers() 只能匹配一种: /admin
    // regexMatchers() 支持正则的匹配

基于方法的权限管理
  • 基于方法的权限管理主要是通过AOP 来实现的,Spring Security 中通过MethodSecurityInterceptor 来提供相关的实现。不同在于Filter Security Interceptor 只是在请求之前进行前置处理,MethodSecurityInterceptor 除了前置处理之外的还可以进行后置处理。前置处理就是在请求之前判断是否具备相应的权限,后置处理规则是对方法的执行结果进行二次过滤。前置处理和后置处理分别对应了不用的实现类。

  • @EnableGlobalMethodSecurity 注解是用来开启权限注解,用法如下

    1
    2
    3
    4
    5
    6
    @Configuration
    @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
    public class SecurityConfig extends WebSecurityConfigurerAdapter {


    }
    • prePostEnabled:开启Spring Security 提供的四个权限注解

      1
      2
      3
      4
      @PostAuthorize: 在目标方式执行之后进行权限校验
      @PostFilter: 在目标方法执行之后对方法的返回结果进行过滤
      @PreAuthorize: 在目标方法执行之前进行权限校验
      @PreFilter: 在目标方法执行之前对方法进行过滤
    • securedEnabled: 开启Spring Security 提供的@Secured 注解支持、该注解不支持权限表达式

      1
      @Secured: 访问目标方法必须具有各相应的角色
    • jsr250Enabled: 开启JSR-250 提供的注解,主要是

      1
      2
      3
      4
      @DenyAll: 拒绝所有访问
      @PermitAll: 允许所有访问
      @RoleAll: 访问目标方法必须具备相应的角色
      这些注解同时也不支持权限表达式

      这些基于方法的权限管理相关的注解,一般来说只要设置prePostEnabled = true 就够用了

  • 测试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    package com.example.controller;

    import com.example.entity.User;
    import org.springframework.security.access.prepost.PostAuthorize;
    import org.springframework.security.access.prepost.PreAuthorize;
    import org.springframework.security.access.prepost.PreFilter;
    import org.springframework.web.bind.annotation.*;

    import java.util.List;

    @RestController
    @RequestMapping("/hello")
    public class AuthorizedController {
    // and / or
    @PreAuthorize("hasRole('ADMIN') and authentication.name=='root'")
    @GetMapping("/hello1")
    public String hello1() {
    return "hello";
    }
    // 是否有权限: READ_INFO
    @PreAuthorize("hasAuthority('READ_INFO')")
    @GetMapping("/hello2")
    public String hello2() {
    return "hello READ_INFO";
    }

    // TODO: 参数必须是认证的用户
    @PreAuthorize("authentication.name==#name")
    @GetMapping("/name")
    public String hello(String name) {
    return "hello: " + name;
    }

    /**
    * filterTarget 必须是 集合/数组
    * 此接口测试需要在浏览器获取 JSESSIONID 的值,通过认证后访问
    */
    @PreFilter(value = "filterObject.id%2!=0", filterTarget = "users")
    @PostMapping("/users")
    public void addUsers(
    @RequestBody List<User> users
    ) {
    System.out.println("users= " + users);
    }

    @PostAuthorize("returnObject.id==1")
    @GetMapping("/userId")
    public User getUserById(Integer id) {
    return new User(id, "coder-itl");
    }
    }

    参数必须是认证的用户
    userId?id=1
  • 对结果集处理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    package com.example.controller;

    import com.example.entity.User;
    import org.springframework.security.access.prepost.PostFilter;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;

    import java.util.ArrayList;
    import java.util.List;

    @RestController
    @RequestMapping("/hello")
    public class AuthorizedController {
    // 用来对方法的返回值进行过滤
    @PostFilter("filterObject.id%2==0")
    @GetMapping("/lists")
    public List<User> getAll() {
    List<User> users = new ArrayList<>();
    for (int i = 0; i < 10; i++) {
    users.add(new User(i, "i: " + i));
    }
    return users;
    }

    }
  • 其他测试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    package com.example.controller;

    import com.example.entity.User;
    import org.springframework.security.access.annotation.Secured;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;

    import javax.annotation.security.DenyAll;
    import javax.annotation.security.PermitAll;
    import javax.annotation.security.RolesAllowed;

    @RestController
    @RequestMapping("/hello")
    public class AuthorizedController {
    // 只能判断角色
    @Secured({"ROLE_USER"})
    @GetMapping("/secured")
    public User getUserByUserName() {
    return new User(99, "coder-itl");
    }

    // 具有其中一个即可
    @Secured({"ROLE_ADMIN", "ROLE_USER"})
    @GetMapping("/username")
    public User getUserByUsername2(String username) {
    return new User(99, username);
    }

    @GetMapping("/denyAll")
    @DenyAll
    public String denyAll() {
    return "DenyAll";
    }

    @GetMapping("/permitAll")
    @PermitAll
    public String permitAll() {
    return "permitAll";
    }

    // 具有其中一个角色即可
    @RolesAllowed({"ROLE_ADMIN", "ROLE_USER"})
    @GetMapping("/rolesAllowed")
    public String rolesAllowed() {
    return "rolesAllowed";
    }
    }

  • 原理分析

    授权原理
    • ConfigAttribute Spring Security 中,用户请求一个资源(通常是一个接口或者一个 Java 方法)需要的角色会被封装成一个ConfigAttribute 对象,在ConfigAttribute 中只有一个getAttribute 方法,该方法返回一个String 字符串,就是角色的名称。一般来说,角色名称都带有一个ROLE 前缀,投票器AccessDecisionVoter 所作的事情,其实就是比较用户所具有各的角色和请求某个资源所需的ConfigAttribute 之间的关系
      • AccesDecisionVoter AccessDecisionManager 都有众多的实现类,在AccessDecisManager 中会挨个遍历AccessDecisionVoter,进而决定是否允许用户访问,因而AccessDecisionVoter AccessDecisionManager 两者的关系类似于AuthenticationProvider ProviderManager 的关系。
授权实战
  • 创建数据表

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    create table user
    (
    id int(11) comment '用户ID',
    username varchar(32) comment '用户名',
    password varchar(255) comment '密码',
    enabled tinyint comment '是否启用',
    locked tinyint comment '是否锁定'
    ) comment '用户表';

    create table user_role
    (
    id int(11),
    uid int(11) comment '用户ID',
    rid int(11) comment '角色ID'
    ) comment '用户角色关系表';

    create table role
    (
    id int(11) comment '角色ID',
    name varchar(32) comment '角色名称(英)',
    nameZh varchar(32) comment '角色名称(中)'
    ) comment '角色表';

    create table menu
    (
    id int(11),
    pattern varchar(128)
    ) comment '菜单表';

    create table menu_role
    (
    id int(11),
    mid int(11),
    rid int(11)
    );
    逆向关系
  • 用户表数据

    1
    2
    3
    insert into user values (1, 'admin', '{noop}admin', 1, 1);
    insert into user values (2, 'user', '{noop}user', 1, 1);
    insert into user values (3, 'coder-itl', '{noop}coder-itl', 1, 1);
  • 角色表数据

    1
    2
    3
    insert into role values (1,'ROLE_ADMIN','系统管理员');
    insert into role values (2,'ROLE_USER','普通用户');
    insert into role values (3,'ROLE_GUEST','游客');
  • 菜单表数据

    1
    2
    3
    insert into menu values (1,'/admin/**');
    insert into menu values (2,'/user/**');
    insert into menu values (3,'/guest/**');
  • 用户角色关系表

    1
    2
    3
    4
    insert into user_role values (1,1,1);
    insert into user_role values (2,1,2);
    insert into user_role values (3,2,2);
    insert into user_role values (4,3,3);
  • 菜单角色数据

    1
    2
    3
    4
    insert into menu_role values (1, 1, 1);
    insert into menu_role values (2, 2, 2);
    insert into menu_role values (3, 3, 3);
    insert into menu_role values (4, 3, 2);
  • 源文件

    https://gitee.com/coder-itl/security-web-template

JWT

  • 前后端权限校验时序图

    时序图

OAuth2

简介
  • 简介

    OAuth2 是一个开放式标准,该标准允许用户让第三方应用访问该用户在某一网站上存储的私密资源(如: 头像、照片、视频等),并且在这个过程中无需将用户名和密码提供给第三方应用。通过令牌(token)可以实现这一功能,每一个令牌授权一个特定的网站在特定的时段内允许可特定的资源。OAtuth 让用户可以授权第三方网站灵活访问他们存储在另外一些资源服务器上的特定信息,而非所有的内容。对于用户而言,我们在互联网应用中最常见的OAuth 应用就是各种第三方登录,例如QQ 授权登录、微信授权登录、微博授权登录等

四种授权模式
  • 授权码模式

    1. 授权码模式: 常见的第三方平台登陆都是使用这种模式
    2. 简化模式: 简化模式是不需要第三方服务端参与,直接在浏览器中向授权服务器申请令牌(token),如果网站是纯静态页面,则可以采用这种方式
    3. 密码模式: 密码模式是用户把用户名/密码直接告诉客户端,客户端使用这些信息后授权服务器申请令牌。这需要用户对客户端高度信任,例如: 客户端应用和服务提供商就是同一家公司。
    4. 客户端模式: 客户端模式是指哭护短使用自己的名义而不是用户的名义向服务提供者申请授权。严格来说,客户端模式并不能算作OAuth 协议解决问题的一种解决方案,但是对于开发者而言,在一些为移动端提供的授权服务器上
  • 流程分析

    流程
    1
    2
    3
    4
    5
    6
    A: 用户打开客户端以后,客户端要求用户给予授权
    B: 用户同意给予客户端授权
    C: 客户端使用上一步获得的授权,向认证服务器申请令牌
    D: 认证服务器对客户端进行认证以后,确认无误,同意发放令牌
    E: 客户端使用令牌,向资源服务器申请获取资源
    F: 资源服务器确认令牌无误,同意向客户端开放资源
  • 四种授权模式·

    • 授权码模式

      • Third-part application: 第三方应用程序,简称客户端(client)

      • Resource Owner: 资源所有者,简称用户(User)

      • User Agent: 用户代理,是指浏览器

      • Authorization Server :认证服务器,即服务端专门用来处理认证的服务器

      • Resource Server: 资源服务器,即服务端存放用户生成的资源的服务器。他于认证服务器,可以是同一台服务器。也可以是不同的服务器

      • 流程图

        授权码模式流程图
        在这里插入图片描述
        1
        2
        3
        4
        5
        A: 用户访问第三方应用,第三方应用通过浏览器导向认证服务器
        B: 用户选择是否给予客户端授权
        C: 假设用户给与授权,认证服务器将用户导向客户端事先指定的`重定向URI`同时附上一个授权码
        D: 客户端收到授权码,附上早先的`重定向 URI`,向认证服务器申请令牌。这一步是在客户端的后台服务器上完成的,对用户不可见
        E: 认证服务器核对授权码和重定向的 URI,确认无误后,向客户端发送访问令牌和更新令牌
        使用第三方应用 导向认证服务器
      • 核心参数

        字段 描述
        client_id 授权服务器注册应用的唯一标识
        response_type 必须固定值在授权码中必须为code
        redirect_uri 必须通过客户端注册的重定向 URL
        scope 必须令牌乐意访问的资源权限 read 只读all 读写
        state 可选存在原样返回客户端,用来防止 CSRF跨站攻击
OAuth2 标准接口
  • /oauth/authorize: 授权端点
  • /oauth/token: 获取令牌端点
  • /oauth/confirm_access: 用户确认授权提交端点
  • /oauth/error: 授权服务错误信息端点
  • /oauth/check_token: 用于资源服务访问的令牌解析端点
  • /oauth/token_key: 提供共有密钥的端点,如果使用JWT 令牌的话
GITHUB 实战
  1. github 创建OAuth 应用

    https://github.com/settings/apps

  2. 生成ID 和密钥

  3. 创建项目

    • 添加依赖

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20

      <dependencies>
      <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
      </dependency>
      <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-security</artifactId>
      </dependency>
      <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-oauth2-client</artifactId>
      </dependency>
      <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
      </dependency>
      </dependencies>
    • 创建配置

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      package com.example.security.config;

      import org.springframework.context.annotation.Configuration;
      import org.springframework.security.config.annotation.web.builders.HttpSecurity;
      import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

      @Configuration
      public class SecurityConfig extends WebSecurityConfigurerAdapter {
      @Override
      protected void configure(HttpSecurity http) throws Exception {
      http.authorizeRequests()
      .anyRequest().authenticated()
      .and()
      // 使用 oauth2 认证
      .oauth2Login();
      }
      }

    • 创建控制器

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      package com.example.controller;

      import org.springframework.security.core.Authentication;
      import org.springframework.security.core.context.SecurityContextHolder;
      import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
      import org.springframework.web.bind.annotation.GetMapping;
      import org.springframework.web.bind.annotation.RestController;

      @RestController
      public class HelloController {
      @GetMapping("/hello")
      public DefaultOAuth2User hello() {
      Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
      return (DefaultOAuth2User) authentication.getPrincipal();
      }
      }

    • 配置

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      server:
      port: 8080

      spring:
      oauth2:
      client:
      registration:
      github:
      client-id: 677f02f9cd2397d6d443
      client-secret: 055520c9be8efcd85d8ea089f85f81633a1be92a
      # 一定要与重定向回调 URL 一致
      redirect-uri: http://localhost:8080/login/oauth2/code/github # /login/oauth2/code/github 固定格式
    • 访问测试

      授权后获取用户信息
    • 原理分析