springboot整合springsecurity+oauth2.0密码授权模式
作者:mmseoamin日期:2023-12-14

springboot整合springsecurity+oauth2.0

本文采用的springboot去整合springsecurity,采用oauth2.0授权认证,使用jwt对token增强。本文仅为学习记录,如有不足多谢提出。

OAuth2 简介

OAuth 2.0是用于授权的行业标准协议。OAuth 2.0为简化客户端开发提供了特定的授权流,包括Web应用、桌面应用、移动端应用等。

OAuth2 相关名词解释

  • Resource owner(资源拥有者):拥有该资源的最终用户,他有访问资源的账号密码;
  • Resource server(资源服务器):拥有受保护资源的服务器,如果请求包含正确的访问令牌,可以访问资源;
  • Client(客户端):访问资源的客户端,会使用访问令牌去获取资源服务器的资源,可以是浏览器、移动设备或者服务器;
  • Authorization server(认证服务器):用于认证用户的服务器,如果客户端认证通过,发放访问资源服务器的令牌

    四种授权模式

    • Authorization Code(授权码模式):正宗的OAuth2的授权模式,客户端先将用户导向认证服务器,登录后获取授权码,然后进行授权,最后根据授权码获取访问令牌;
    • Implicit(简化模式):和授权码模式相比,取消了获取授权码的过程,直接获取访问令牌;
    • Resource Owner Password Credentials(密码模式):客户端直接向用户获取用户名和密码,之后向认证服务器获取访问令牌;
    • Client Credentials(客户端模式):客户端直接通过客户端认证(比如client_id和client_secret)从认证服务器获取访问令牌。

      主要pom文件引入

      
              
                  org.springframework.boot
                  spring-boot-starter-security
              
      
              
                  org.springframework.security.oauth
                  spring-security-oauth2
                  2.2.6.RELEASE
              
              
              
                  org.springframework.security
                  spring-security-jwt
                  1.1.0.RELEASE
              
              
       #本文采用的springboot版本为2.6.3,由于Spring Security 在 Spring Boot 2.7.0 中已弃用的 WebSecurityConfigurerAdapter
       所有在配置。所以在配置SpringSecurity配置时,原先configure采用bena配置SecurityFilterChain bean
      

      编写实体类

      用户类

      package com.example.health.model;
      import com.alibaba.fastjson.annotation.JSONField;
      import lombok.Data;
      import org.springframework.security.core.GrantedAuthority;
      import org.springframework.security.core.authority.SimpleGrantedAuthority;
      import org.springframework.security.core.userdetails.UserDetails;
      import java.util.Collection;
      import java.util.Set;
      /**
       * 登录用户信息
       */
      @Data
      public class SecurityUser implements UserDetails {
          /**
           * 用户id
           */
          private Long userId;
          /**
           * 用户名
           */
          private String username;
          /**
           * 部门ID
           */
          private Long deptId;
          /**
           * 用户密码
           */
          private String password;
          /**
           * 用户状态
           */
          private Boolean enabled;
          /**
           * 权限数据
           */
          private Collection authorities;
          /**
           * 权限列表
           */
          private Set permissions;
          public SecurityUser() {
          }
          @Override
          public Collection getAuthorities() {
              return this.authorities;
          }
          @Override
          public String getPassword() {
              return this.password;
          }
          @Override
          public String getUsername() {
              return this.username;
          }
          /**
           * 账户是否未过期,过期无法验证
           */
          @JSONField(serialize = false)
          @Override
          public boolean isAccountNonExpired() {
              return true;
          }
          /**
           * 指定用户是否解锁,锁定的用户无法进行身份验证
           *
           * @return
           */
          @Override
          public boolean isAccountNonLocked() {
              return true;
          }
          /**
           * 指示是否已过期的用户的凭据(密码),过期的凭据防止认证
           *
           * @return
           */
          @Override
          public boolean isCredentialsNonExpired() {
              return true;
          }
          /**
           * 是否可用 ,禁用的用户不能身份验证
           *
           * @return
           */
          @Override
          public boolean isEnabled() {
              return this.enabled;
          }
      }
      

      auth2获取Token返回信息封装

      package com.example.health.model.dto;
      import lombok.Builder;
      import lombok.Data;
      import lombok.EqualsAndHashCode;
      /**
       * Oauth2获取Token返回信息封装
       */
      @Data
      @EqualsAndHashCode(callSuper = false)
      @Builder
      public class Oauth2TokenDto {
          /**
           * 访问令牌
           */
          private String token;
          /**
           * 刷新令牌
           */
          private String refreshToken;
          /**
           * 访问令牌头前缀
           */
          private String tokenHead;
          /**
           * 有效时间(秒)
           */
          private int expiresIn;
      }
      

      添加UserServiceImpl实现UserDetailsService接口,用于加载用户信息:

      package com.example.health.security.handle;
      import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
      import com.example.health.common.constant.MessageConstant;
      import com.example.health.mapper.SysUserMapper;
      import com.example.health.model.SecurityUser;
      import com.example.health.model.entity.SysUser;
      import org.springframework.beans.BeanUtils;
      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 java.util.Objects;
      @Service
      public class UserServiceImpl implements UserDetailsService {
          @Autowired
          private SysUserMapper sysUserMapper;
          @Override
          public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
              SysUser sysUser = sysUserMapper.selectOne(new QueryWrapper().lambda().eq(SysUser::getUserName, username));
              if (Objects.isNull(sysUser)) {
                  throw new UsernameNotFoundException(MessageConstant.USERNAME_PASSWORD_ERROR);
              }
              SecurityUser securityUser = new SecurityUser();
              BeanUtils.copyProperties(sysUser, securityUser);
              securityUser.setEnabled(!Objects.equals(0, sysUser.getStatus()));
              return securityUser;
          }
      }
      

      添加AuthenticationEntryPointImpl实现AuthenticationEntryPoint接口,用于处理失败处理类 :

      /**
       * 认证失败处理类 返回未授权
       *
       */
      @Component
      public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint, Serializable {
          private static final long serialVersionUID = -8970718410437077606L;
          @Override
          public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
              int code = HttpStatus.UNAUTHORIZED;
              String msg = String.format("请求访问:%s,认证失败,无法访问系统资源", request.getRequestURI());
              ServletUtils.renderString(response, JSON.toJSONString(ResultUtils.error(code, msg)));
          }
      }
      

      配置JWT内容增强器

      /**
       * JWT内容增强器
       */
      @Component
      public class JwtTokenEnhancer implements TokenEnhancer {
          @Override
          public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
              SecurityUser securityUser = (SecurityUser) authentication.getPrincipal();
              Map info = new HashMap<>();
              //把用户ID设置到JWT中
              info.put("user_id", securityUser.getUserId());
              ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(info);
              return accessToken;
          }
      }
      

      添加认证服务器配置,使用@EnableAuthorizationServer注解开启:

      /**
       * 认证服务配置
       */
      @AllArgsConstructor
      @Configuration
      @EnableAuthorizationServer
      public class Oauth2Config extends AuthorizationServerConfigurerAdapter {
          private PasswordEncoder passwordEncoder;
          private UserServiceImpl userDetailsService;
          /**
           * 该对象用来支持 password 模式
           */
          private AuthenticationManager authenticationManager;
          private JwtTokenEnhancer jwtTokenEnhancer;
          @Override
          public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
              clients.inMemory()
                      .withClient("client-app")
                      .secret(passwordEncoder.encode("123456"))
                      .scopes("all")
                      .authorizedGrantTypes("password", "refresh_token")
                      .accessTokenValiditySeconds(3600)
                      .refreshTokenValiditySeconds(86400);
          }
          @Override
          public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
              TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
              List delegates = new ArrayList<>();
              delegates.add(jwtTokenEnhancer);
              delegates.add(accessTokenConverter());
              enhancerChain.setTokenEnhancers(delegates); //配置JWT的内容增强器
              endpoints.authenticationManager(authenticationManager)
                      .userDetailsService(userDetailsService) //配置加载用户信息的服务
                      .accessTokenConverter(accessTokenConverter())
                      .tokenEnhancer(enhancerChain);
          }
          @Override
          public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
              security.allowFormAuthenticationForClients();
          }
          @Bean
          public JwtAccessTokenConverter accessTokenConverter() {
              JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
              jwtAccessTokenConverter.setKeyPair(keyPair());
              return jwtAccessTokenConverter;
          }
          @Bean
          public KeyPair keyPair() {
              //从classpath下的证书中获取秘钥对
              KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), "123456".toCharArray());
              return keyStoreKeyFactory.getKeyPair("jwt", "123456".toCharArray());
          }
      }
      

      添加SpringSecurity配置,允许认证相关路径的访问及表单登录:

      /**
       * SpringSecurity配置
       */
      @Configuration
      @EnableWebSecurity
      public class SecurityConfig extends WebSecurityConfigurerAdapter {
          /**
           * 认证失败处理类
           */
          @Autowired
          private AuthenticationEntryPointImpl unauthorizedHandler;
          @Bean
          @Override
          public AuthenticationManager authenticationManagerBean() throws Exception {
              return super.authenticationManagerBean();
          }
          /**
           * anyRequest          |   匹配所有请求路径
           * access              |   SpringEl表达式结果为true时可以访问
           * anonymous           |   匿名可以访问
           * denyAll             |   用户不能访问
           * fullyAuthenticated  |   用户完全认证可以访问(非remember-me下自动登录)
           * hasAnyAuthority     |   如果有参数,参数表示权限,则其中任何一个权限可以访问
           * hasAnyRole          |   如果有参数,参数表示角色,则其中任何一个角色可以访问
           * hasAuthority        |   如果有参数,参数表示权限,则其权限可以访问
           * hasIpAddress        |   如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
           * hasRole             |   如果有参数,参数表示角色,则其角色可以访问
           * permitAll           |   用户可以任意访问
           * rememberMe          |   允许通过remember-me登录的用户访问
           * authenticated       |   用户登录后可访问
           */
          @Override
          protected void configure(HttpSecurity httpSecurity) throws Exception {
              httpSecurity
                      // CSRF禁用,因为不使用session
                      .csrf().disable()
                      // 禁用HTTP响应标头
                      .headers().cacheControl().disable().and()
                      // 认证失败处理类
                      .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
                      // 基于token,所以不需要session
                      .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                      // 过滤请求
                      .authorizeRequests()
                      // 对于登录login 注册register 验证码captchaImage 允许匿名访问
                      .antMatchers("/login", "/register", "/captchaImage").permitAll()
                      .antMatchers("/all/**").permitAll()
                      // 静态资源,可匿名访问
                      .antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll()
                      .antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll()
                      // 除上面外的所有请求全部需要鉴权认证
                      .anyRequest().authenticated()
                      .and()
                      .headers().frameOptions().disable();
          }
          @Bean
          public PasswordEncoder passwordEncoder() {
              return new BCryptPasswordEncoder();
          }
      }
      

      编写登录接口,以及测试接口

      /**
       * 自定义Oauth2获取令牌接口
       */
      @RestController
      @RequestMapping("/oauth")
      public class AuthController {
          @Autowired
          private TokenEndpoint tokenEndpoint;
          /**
           * Oauth2登录认证
           */
          @RequestMapping(value = "/token", method = RequestMethod.POST)
          public BaseResponse postAccessToken(Principal principal, @RequestParam Map parameters) throws HttpRequestMethodNotSupportedException, HttpRequestMethodNotSupportedException {
              OAuth2AccessToken oAuth2AccessToken = tokenEndpoint.postAccessToken(principal, parameters).getBody();
              Oauth2TokenDto oauth2TokenDto = Oauth2TokenDto.builder()
                      .token(oAuth2AccessToken.getValue())
                      .refreshToken(oAuth2AccessToken.getRefreshToken().getValue())
                      .expiresIn(oAuth2AccessToken.getExpiresIn())
                      .tokenHead("Bearer ").build();
              return ResultUtils.success(oauth2TokenDto);
          }
      }
      
      @RestController
      @RequestMapping("/all")
      public class AllController {
          @GetMapping(value = "/getStr")
          public BaseResponse getStr() {
              return ResultUtils.success(“All”);
          }
      }
      
      @RestController
      @RequestMapping("/test")
      public class TestController {
          @GetMapping(value = "/getStr")
          public BaseResponse getStr()  {
              return ResultUtils.success("test");
          }
      }
      

      测试使用password授权方式

      springboot整合springsecurity+oauth2.0密码授权模式,在这里插入图片描述,第1张

      springboot整合springsecurity+oauth2.0密码授权模式,在这里插入图片描述,第2张

      springboot整合springsecurity+oauth2.0密码授权模式,在这里插入图片描述,第3张