Form Login and OAuth2 Login with Spring Security

Architecture

Please refer to https://docs.spring.io/spring-security/reference/servlet/architecture.html and https://docs.spring.io/spring-security/reference/servlet/oauth2/resource-server/index.html

Key Dependencies

implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'

Notes: oauth2-client引入第三方登录oauth2Login(),oauth2-resource-server 引入BearerTokenAuthenticationFilter,使得所配置的api都要经过JWT验证

Key Configuration

SecurityConfig.java

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) // Enable "@PreAuthorize"
public class SecurityConfig {

  private final Oauth2SuccessHandler oauth2SuccessHandler;
  private final Oauth2FailureHandler oauth2FailureHandler;

  public SecurityConfig(
      @Lazy Oauth2SuccessHandler oauth2SuccessHandler, Oauth2FailureHandler oauth2FailureHandler) {
    this.oauth2SuccessHandler = oauth2SuccessHandler;
    this.oauth2FailureHandler = oauth2FailureHandler;
  }

  @Bean
  public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.cors()
        .and()
        .csrf() // There is no csrf vulnerability if we don't use cookie and session.
        .disable()
        .authorizeRequests()
        .antMatchers("/users/login")
        .anonymous() // 所有未登录的用户可以访问
        .antMatchers(HttpMethod.POST, "/users")
        .permitAll() // 所有用户都能访问
        .anyRequest()
        .authenticated() // 其他API由Spring Security保护
        .and()
        .oauth2Login() // 第三方登录
        .successHandler(oauth2SuccessHandler) // 登录成功
        .failureHandler(oauth2FailureHandler) // 登录失败
        .and()
        .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt) // 强制受保护的API需要JWT验证,由BearerTokenAuthenticationFilter和JwtAuthenticationProvider来实现
        .sessionManagement()
        .sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    return http.build();
  }

  @Bean
  public AuthenticationManager authenticationManager(
      AuthenticationConfiguration authenticationConfiguration) throws Exception {
    return authenticationConfiguration.getAuthenticationManager();
  }

  @Bean
  public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
  }
}

Notes: Spring Security的formLogin()功能太弱,如果加上http.formLogin(),随之而引入的UsernamePasswordAuthenticationFilter只能处理表单形式提交的username和password,而如今大部分前端应用都是以JSON格式将参数放在post的body里提交的。虽然可以重写attemptAuthentication()方法并通过request.getInputStream()来获取body的信息,但servlet的request只能调用getInputStream方法一次,此处调用后,后续的filter chain上的filter就不能在调用了,因此不如弃用该功能,我们自己在service里实现用户通过表单登录的认证功能。

UserDetailServiceImpl.java

@Service
public class UserDetailServiceImpl implements UserDetailsService {

  @Autowired private UserRepository userRepository;

  @Override
  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    User user = userRepository.findByUsername(username); // 从数据库中查询该用户
    if (null == user) {
      throw new UsernameNotFoundException(String.format("''%s does not exist.", username));
    }
    return org.springframework.security.core.userdetails.User.withUsername(username)
        .password(user.getPassword())
        .authorities(user.getUserType())
        .build(); // 组装成Spring Security需要的类型,后续这个的Filters进行认证时,会用这个对象与所传参数进行对比
  }
}

自己实现表单认证功能的部分代码

UsernamePasswordAuthenticationToken authRequest =
        new UsernamePasswordAuthenticationToken(username, password); // 从前端请求中得到的用户名与密码,将之组合成Spring Security方便处理的格式
Authentication authenticationResult = authenticationManager.authenticate(authRequest); // Spring Security进行认证处理。该authRequest是UsernamePasswordAuthenticationToken类型,因此AuthenticationManager会选择DaoAuthenticationProvider来进行验证。

if (null == authenticationResult) {

    // 认证失败……
}

// 认证成功…… 生成JWT返给前端

第三方认证登录

第三方认证登录只需简单配置即可,主要是登录成功后的动作

Oauth2SuccessHandler.java

@Slf4j
@Component
public class Oauth2SuccessHandler implements AuthenticationSuccessHandler {
  @Override
  public void onAuthenticationSuccess(
      HttpServletRequest request, HttpServletResponse response, Authentication authentication)
      throws IOException, ServletException {
    // 第三方登录成功,提取第三方登录成功后返还给我们的信息,以下是业务逻辑相关代码,无需参考
    OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal();
    String username = (String) oAuth2User.getAttribute("name") + oAuth2User.getAttribute("email");
    User user = ums.findByName(username);
    if (null == user) {
      user = new User();
      user.setUsername(username);
      user.setPassword(username);
      user.setUserType(UserType.CUSTOMER.toString());
      user.setStatus(UserStatus.ACTIVE.toString());
      user = ums.signup(user);
    }
    String token = jwtUtil.generateToken(user); // 生成JWT

    ObjectNode msgNode = objectMapper.createObjectNode();
    msgNode.put("token", token);
    msgNode.put(USER_TYPE, user.getUserType());
    FilterResponseUtil.ok(response, msgNode);
  }
}

application.yml

spring:
  security:
    oauth2:
      client:
        registration:
          github:
            clientId: ${GITHUB_CLIENT_ID}
            clientSecret: ${GITHUB_CLIENT_SECRET}
            redirect-uri: https:///login/oauth2/code/github # 由于我们的服务经常跑在反向代理后面,如果不指明redirect-uri,则默认使用反向代理实际请求后端的uri,从而出错,因此最好指明重定向uri
          google:
            clientId: ${GOOGLE_CLIENT_ID}
            clientSecret: ${GOOGLE_CLIENT_SECRET}
            redirect-uri: https:///login/oauth2/code/google
      resource-server: # 对于受保护的API, 所有的请求都要验证JWT。注意需要权限验证的话,JWT的payload里要有scope字段,然后在相应的api上加上类似于@PreAuthorize("hasAuthority('SCOPE_merchant')")的注解
        jwt: # The default jwt decoder implementation is NimbusJwtDecoder 
          jws-algorithm: RS256
          public-key-location: file:${JWT_PUBLIC_KEY:/rsa-secrets/jwt_key.pub}

你可能感兴趣的:(Form Login and OAuth2 Login with Spring Security)