前言
这一篇文章是项目框架的重头戏,在找了多方文章后才从这一系列博客–>Spring Cloud实战 | 第六篇:Spring Cloud Gateway + Spring Security OAuth2 + JWT实现微服务统一认证鉴权成功的搭建了OAuth2框架。
跟随上面介绍的博客,使用Gateway作为资源服务器,OAuth2作为认证服务器,使用JWT作为token。搭建了这个框架。在搭建完成后写下这一篇博客记录项目搭建过程和所遇到的问题。
概念介绍
这一模块留待后续补充,目前先介绍项目搭建过程。
认证服务器
Spring Security OAuth过时
在最开始使用OAuth2时,我才发现Spring Security OAuth
已过时,Spring官方已经OAuth2整合到Spring Security中,但是经过多次搜索,我依旧没找到基于Spring Security如何使用OAuth2,所以决定使用spring-cloud-starter-oauth2
先用着,等日后博客更多或者我的技术更好再进行更换,也算留个坑。
Spring Security OAuth
导入依赖
1 2 3 4 5 6 7 8 9
| <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> </dependency>
<dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-oauth2-jose</artifactId> </dependency>
|
编写配置文件
和其他微服务相同,通用配置就够了不需要添加特殊的配置
1 2 3 4 5 6 7 8 9
| spring.application.name=@project.artifactId@ spring.profiles.active=@profiles.active@ spring.main.allow-bean-definition-overriding=true
spring.cloud.nacos.discovery.server-addr=@profiles.nacos@ spring.cloud.nacos.config.server-addr=@profiles.nacos@ spring.cloud.nacos.config.file-extension=properties spring.cloud.nacos.config.shared-dataids=application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension} spring.cloud.nacos.config.refreshable-dataids=application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
|
认证服务器配置
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
|
@Configuration @EnableAuthorizationServer @AllArgsConstructor public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
private AuthenticationManager authenticationManager; private UserDetailServiceImpl userDetailsService;
@Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients .inMemory() .withClient(AuthConstants.ADMIN_CLIENT_ID) .secret(new BCryptPasswordEncoder().encode(AuthConstants.ADMIN_CLIENT_SECRET)) .authorizedGrantTypes("password", "refresh_token") .scopes("all") .autoApprove(false); }
@Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) { TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain(); List<TokenEnhancer> tokenEnhancers = new ArrayList<>(); tokenEnhancers.add(tokenEnhancer()); tokenEnhancers.add(jwtAccessTokenConverter()); tokenEnhancerChain.setTokenEnhancers(tokenEnhancers);
endpoints.authenticationManager(authenticationManager) .accessTokenConverter(jwtAccessTokenConverter()) .tokenEnhancer(tokenEnhancerChain) .userDetailsService(userDetailsService) .reuseRefreshTokens(false); }
@Override public void configure(AuthorizationServerSecurityConfigurer security) { CustomClientCredentialsTokenEndpointFilter endpointFilter = new CustomClientCredentialsTokenEndpointFilter(security); endpointFilter.afterPropertiesSet(); endpointFilter.setAuthenticationEntryPoint(authenticationEntryPoint()); security.addTokenEndpointAuthenticationFilter(endpointFilter);
security.authenticationEntryPoint(authenticationEntryPoint()) .tokenKeyAccess("isAuthenticated()") .checkTokenAccess("permitAll()"); }
@Bean public JwtAccessTokenConverter jwtAccessTokenConverter() { JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); converter.setKeyPair(keyPair()); return converter; }
@Bean public KeyPair keyPair() { KeyStoreKeyFactory factory = new KeyStoreKeyFactory( new ClassPathResource("shiming.jks"), "123456".toCharArray()); KeyPair keyPair = factory.getKeyPair( "shiming", "123456".toCharArray()); return keyPair; }
@Bean public TokenEnhancer tokenEnhancer() { return (accessToken, authentication) -> { Map<String, Object> map = new HashMap<>(2); OAuthUser user = (OAuthUser) authentication.getUserAuthentication().getPrincipal(); map.put(AuthConstants.JWT_USER_ID_KEY, user.getId()); map.put(AuthConstants.JWT_CLIENT_ID_KEY, user.getClientId()); map.put(AuthConstants.JWT_NICKNAME,user.getUser().getNickname()); ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(map); return accessToken; }; }
@Bean public AuthenticationEntryPoint authenticationEntryPoint() { return (request, response, e) -> { response.setStatus(HttpStatus.HTTP_OK); response.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_UTF8_VALUE); response.setHeader("Access-Control-Allow-Origin", "*"); response.setHeader("Cache-Control", "no-cache"); ResultDto result = ResultDto.error(ErrorCodeEnum.USER_ERROR_A0303); response.getWriter().print(JSONUtil.toJsonStr(result)); response.getWriter().flush(); }; } }
|
生成密钥
1 2 3 4 5 6 7 8 9 10 11 12 13
| keytool -genkey -alias shiming -keyalg RSA -keypass 123456 -keystore shiming.jks -storepass 123456
-genkey 生成密钥
-alias 别名
-keyalg 密钥算法
-keypass 密钥口令
-keystore 生成密钥库的存储路径和名称
-storepass 密钥库口令
|
编写获取公钥接口
1 2 3 4 5 6 7 8
| @ApiOperation("获取公钥") @GetMapping("getPublicKey") public Map<String, Object> getPublicKey() { RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); RSAKey key = new RSAKey.Builder(publicKey).build(); return new JWKSet(key).toJSONObject(); }
|
安全配置
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
|
@Configuration @AllArgsConstructor @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/oauth/**","/v2/api-docs") .permitAll() .anyRequest().authenticated() .and() .formLogin() .and() .csrf().disable();
}
@Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); }
@Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }
}
|
客户端自定义异常处理
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
|
public class CustomClientCredentialsTokenEndpointFilter extends ClientCredentialsTokenEndpointFilter {
private AuthorizationServerSecurityConfigurer configurer; private AuthenticationEntryPoint authenticationEntryPoint;
public CustomClientCredentialsTokenEndpointFilter(AuthorizationServerSecurityConfigurer configurer) { this.configurer = configurer; }
@Override public void setAuthenticationEntryPoint(AuthenticationEntryPoint authenticationEntryPoint) { super.setAuthenticationEntryPoint(null); this.authenticationEntryPoint = authenticationEntryPoint; }
@Override protected AuthenticationManager getAuthenticationManager() { return configurer.and().getSharedObject(AuthenticationManager.class); }
@Override public void afterPropertiesSet() { setAuthenticationFailureHandler((request, response, e) -> authenticationEntryPoint.commence(request, response, e)); setAuthenticationSuccessHandler((request, response, authentication) -> { }); } }
|
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
| @RestControllerAdvice @Slf4j public class AuthExceptionHandler {
@ExceptionHandler(InvalidGrantException.class) public ResultDto<?> handleInvalidGrantException(InvalidGrantException e) { return ResultDto.error(ErrorCodeEnum.USER_ERROR_A0210); }
@ExceptionHandler({InternalAuthenticationServiceException.class}) public ResultDto<?> handleInternalAuthenticationServiceException(InternalAuthenticationServiceException e) { return ResultDto.error(e.getMessage(),ErrorCodeEnum.USER_ERROR_A0200); } }
|
自定义token
创建用户信息类接受feign调用其他服务返回的值
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
| @Data @EqualsAndHashCode(callSuper = false) @Accessors(chain = true) public class User implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.ASSIGN_ID) private Long id;
private String account;
private String name;
private String nickname;
private String password;
private String introduction;
private Integer sex;
private String telephone;
private String email;
private String avatarUrl;
private String backgroundUrl;
private Integer pushMethod;
@TableLogic private Integer isDeleted;
private Long orgId;
private String orgName;
private String orgPath;
@TableField(fill = FieldFill.INSERT) private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updateTime;
private String def1;
private String def2;
private String def3;
private String def4;
private String def5;
private List<Long> roles; }
|
自定义token用户类
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
| @Data @AllArgsConstructor @NoArgsConstructor public class OAuthUser implements UserDetails { private Long id; private String username; private String password; private Boolean enabled; private String clientId; private User user; private Collection<SimpleGrantedAuthority> authorities;
public OAuthUser(User user) { this.user = user; this.setId(user.getId()); this.setUsername(user.getAccount()); this.setPassword(user.getPassword()); this.setEnabled(0 == user.getIsDeleted()); if (CollectionUtil.isNotEmpty(user.getRoles())) { authorities = new ArrayList<>(); user.getRoles().forEach(roleId -> authorities.add(new SimpleGrantedAuthority(String.valueOf(roleId)))); } }
@Override public Collection<? extends GrantedAuthority> getAuthorities() { return this.authorities; }
@Override public String getPassword() { return password; }
@Override public String getUsername() { return username; }
@Override public boolean isAccountNonExpired() { return true; }
@Override public boolean isAccountNonLocked() { return true; }
@Override public boolean isCredentialsNonExpired() { return true; }
@Override public boolean isEnabled() { return enabled; } }
|
自定义UserDetailService
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
| @Service @AllArgsConstructor @Slf4j public class UserDetailServiceImpl implements UserDetailsService {
private SystemFeign systemFeign;
private HttpServletRequest request;
@Override public UserDetails loadUserByUsername(String account) throws UsernameNotFoundException { String clientId = request.getParameter(AuthConstants.JWT_CLIENT_ID_KEY); OAuthUser oAuthUser = null; switch (clientId) { case AuthConstants.ADMIN_CLIENT_ID: ResultDto<User> res = systemFeign.getUserInfo(account); if (!res.isSuccess()) { throw new UsernameNotFoundException(ErrorCodeEnum.USER_ERROR_A0201.getDescription()); } User user = res.getData(); oAuthUser = new OAuthUser(user); oAuthUser.setClientId(clientId); break; default: } if (!oAuthUser.isEnabled()) { throw new DisabledException("该账户已被禁用!"); } else if (!oAuthUser.isAccountNonLocked()) { throw new LockedException("该账号已被锁定!"); } else if (!oAuthUser.isAccountNonExpired()) { throw new AccountExpiredException("该账号已过期!"); } return oAuthUser; } }
|
重写之后的这个方法会通过传入的username作为参数调用其他微服务的方法,最后将获取到的加密密码和传入的密码加密后比较。相同则调用接下来的方法,不同会抛出密码错误异常,而之前所写的异常处理拦截器会拦截异常将密码错误返回给前端。
自定义返回信息
1 2 3 4 5 6 7 8 9 10 11
| @Data @Builder public class Oauth2Token {
private String token;
private String refreshToken;
private int expiresIn;
}
|
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
| @ApiOperation("获取token") @ApiImplicitParams({ @ApiImplicitParam(name = "grant_type", defaultValue = "password", value = "授权模式", required = true), @ApiImplicitParam(name = "client_id", defaultValue = "shimingWeb", value = "Oauth2客户端ID", required = true), @ApiImplicitParam(name = "client_secret", defaultValue = "shimingSecret", value = "Oauth2客户端秘钥", required = true), @ApiImplicitParam(name = "refresh_token", value = "刷新token"), @ApiImplicitParam(name = "username", defaultValue = "17748725080", value = "登录账户"), @ApiImplicitParam(name = "password", defaultValue = "123456", value = "登录密码"),
//@ApiImplicitParam(name = "code", value = "小程序code"), //@ApiImplicitParam(name = "encryptedData", value = "包括敏感数据在内的完整用户信息的加密数据"), //@ApiImplicitParam(name = "iv", value = "加密算法的初始向量"), }) @PostMapping("token") public ResultDto<?> postAccessToken( Principal principal, @RequestParam Map<String, String> parameters ) throws HttpRequestMethodNotSupportedException { OAuth2AccessToken oAuth2AccessToken = tokenEndpoint.postAccessToken(principal, parameters).getBody(); assert oAuth2AccessToken != null; Oauth2Token oauth2Token = Oauth2Token.builder() .token(oAuth2AccessToken.getValue()) .refreshToken(oAuth2AccessToken.getRefreshToken().getValue()) .expiresIn(oAuth2AccessToken.getExpiresIn()) .build(); return ResultDto.success(oauth2Token); }
|
注销
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| @ApiOperation("注销") @GetMapping("/logout") public ResultDto<?> logout(HttpServletRequest request) { String payload = request.getHeader(AuthConstants.JWT_PAYLOAD_KEY); JSONObject jsonObject = JSONUtil.parseObj(payload); String jti = jsonObject.getStr("jti"); long exp = jsonObject.getLong("exp");
long currentTimeSeconds = System.currentTimeMillis() / 1000; if (exp < currentTimeSeconds) { return ResultDto.error(ErrorCodeEnum.USER_ERROR_A0311); } RedisUtil.set(AuthConstants.TOKEN_BLACKLIST_PREFIX + jti, null, (exp - currentTimeSeconds), TimeUnit.SECONDS); return ResultDto.success(); }
|
如上,oauth2的认证服务器就完成了。
原文链接
Spring Cloud实战 | 第六篇:Spring Cloud Gateway + Spring Security OAuth2 + JWT实现微服务统一认证鉴权