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}