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://<Your URI>/login/oauth2/code/github # 由于我们的服务经常跑在反向代理后面,如果不指明redirect-uri,则默认使用反向代理实际请求后端的uri,从而出错,因此最好指明重定向uri
          google:
            clientId: ${GOOGLE_CLIENT_ID}
            clientSecret: ${GOOGLE_CLIENT_SECRET}
            redirect-uri: https://<Your URI>/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}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,816评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,729评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,300评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,780评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,890评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,084评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,151评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,912评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,355评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,666评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,809评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,504评论 4 334
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,150评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,882评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,121评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,628评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,724评论 2 351

推荐阅读更多精彩内容