封面图片

微服务

SpringCloud微服务开发(三):搭建认证服务器

微服务体系一般由服务注册发现中心、网关、资源服务和认证服务这几个部分组成。这是微服务系列的第三篇,认证服务器搭建。


在开始开发认证服务器之前,需要先做一些准备工作。包括生成RSA证书,用来加密密码进行传输;部署redis,用来存放token。token的存放方式有多种,可以是内存的,数据库的,也可以是redis。本次使用redis存储token,同时用来存储权限信息。

生成RSA证书

1keytool -genkey -alias jwt -keyalg RSA -keystore jwt.jks

将生成的证书放到resources下。记住生成证书时输入的密码。

Get Start

pom.xml

1<?xml version="1.0" encoding="UTF-8"?> 2<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 3 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> 4 <modelVersion>4.0.0</modelVersion> 5 <parent> 6 <groupId>org.springframework.boot</groupId> 7 <artifactId>spring-boot-starter-parent</artifactId> 8 <version>2.3.4.RELEASE</version> 9 <relativePath/> <!-- lookup parent from repository --> 10 </parent> 11 <groupId>com.example</groupId> 12 <artifactId>oauth-service</artifactId> 13 <version>0.0.1-SNAPSHOT</version> 14 <name>oauth-service</name> 15 <description>oauth-service</description> 16 <properties> 17 <java.version>1.8</java.version> 18 <org.projectlombok.version>1.18.16</org.projectlombok.version> 19 </properties> 20 <dependencyManagement> 21 <dependencies> 22 <!-- spring cloud 依赖 --> 23 <dependency> 24 <groupId>org.springframework.cloud</groupId> 25 <artifactId>spring-cloud-dependencies</artifactId> 26 <version>Hoxton.SR12</version> 27 <type>pom</type> 28 <scope>import</scope> 29 </dependency> 30 <!-- spring cloud alibaba 依赖--> 31 <dependency> 32 <groupId>com.alibaba.cloud</groupId> 33 <artifactId>spring-cloud-alibaba-dependencies</artifactId> 34 <version>2.2.1.RELEASE</version> 35 <type>pom</type> 36 <scope>import</scope> 37 </dependency> 38 </dependencies> 39 </dependencyManagement> 40 <dependencies> 41 <dependency> 42 <groupId>com.alibaba.cloud</groupId> 43 <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> 44 </dependency> 45 <dependency> 46 <groupId>org.springframework.boot</groupId> 47 <artifactId>spring-boot-starter</artifactId> 48 </dependency> 49 <dependency> 50 <groupId>org.springframework.boot</groupId> 51 <artifactId>spring-boot-starter-web</artifactId> 52 </dependency> 53 <dependency> 54 <groupId>org.springframework.boot</groupId> 55 <artifactId>spring-boot-starter-security</artifactId> 56 </dependency> 57 <dependency> 58 <groupId>org.springframework.cloud</groupId> 59 <artifactId>spring-cloud-starter-oauth2</artifactId> 60 </dependency> 61 <dependency> 62 <groupId>org.springframework.boot</groupId> 63 <artifactId>spring-boot-starter-data-jpa</artifactId> 64 </dependency> 65 <dependency> 66 <groupId>com.nimbusds</groupId> 67 <artifactId>nimbus-jose-jwt</artifactId> 68 <version>8.16</version> 69 </dependency> 70 <!-- 引入Swagger3依赖 --> 71 <dependency> 72 <groupId>io.springfox</groupId> 73 <artifactId>springfox-boot-starter</artifactId> 74 <version>3.0.0</version> 75 </dependency> 76 <!-- redis --> 77 <dependency> 78 <groupId>org.springframework.boot</groupId> 79 <artifactId>spring-boot-starter-data-redis</artifactId> 80 </dependency> 81 <dependency> 82 <groupId>org.springframework.boot</groupId> 83 <artifactId>spring-boot-devtools</artifactId> 84 <scope>runtime</scope> 85 <optional>true</optional> 86 </dependency> 87 <dependency> 88 <groupId>mysql</groupId> 89 <artifactId>mysql-connector-java</artifactId> 90 <scope>runtime</scope> 91 </dependency> 92 <dependency> 93 <groupId>org.springframework.boot</groupId> 94 <artifactId>spring-boot-configuration-processor</artifactId> 95 <optional>true</optional> 96 </dependency> 97 <dependency> 98 <groupId>org.projectlombok</groupId> 99 <artifactId>lombok</artifactId> 100 <optional>true</optional> 101 </dependency> 102 <dependency> 103 <groupId>org.springframework.boot</groupId> 104 <artifactId>spring-boot-starter-test</artifactId> 105 <scope>test</scope> 106 </dependency> 107 </dependencies> 108 109 <build> 110 <plugins> 111 <plugin> 112 <groupId>org.springframework.boot</groupId> 113 <artifactId>spring-boot-maven-plugin</artifactId> 114 <configuration> 115 <excludes> 116 <exclude> 117 <groupId>org.projectlombok</groupId> 118 <artifactId>lombok</artifactId> 119 </exclude> 120 </excludes> 121 </configuration> 122 </plugin> 123 </plugins> 124 </build> 125 126</project>

application.yml

1server: 2 port: 9092 3 4spring: 5 application: 6 name: oauth-service 7 datasource: 8 url: jdbc:mysql://localhost:3306/demo?databaseTerm=SCHEMA&useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&useAffectedRows=true&useSSL=false&serverTimezone=Asia/Shanghai 9 username: root 10 password: 123456 11 driverClassName: com.mysql.cj.jdbc.Driver 12 hikari: 13 max-lifetime: 120000 14 jpa: 15 hibernate: 16 ddl-auto: update 17 jackson: 18 date-format: yyyy-MM-dd HH:mm:ss 19 redis: 20 host: localhost 21 port: 6381 22 password: 123456 23 timeout: 3000 24 maxIdle: 10 25 maxWaitMillis: 1500 26 blockWhenExhausted: false 27 cloud: 28 nacos: 29 discovery: 30 server-addr: localhost:8848 31 username: nacos 32 password: nacos 33 gateway: 34 discovery: 35 locator: 36 enabled: true # gateway 可以从 nacos 发现微服务 37management: 38 endpoints: 39 web: 40 exposure: 41 include: '*'

redis连接配置类:

1@Configuration 2@EnableRedisRepositories 3public class RedisRepositoryConfig { 4 private final RedisConnectionFactory redisConnectionFactory; 5 6 public RedisRepositoryConfig(RedisConnectionFactory redisConnectionFactory) { 7 this.redisConnectionFactory = redisConnectionFactory; 8 } 9 10 @Bean 11 public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) { 12 RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>(); 13 redisTemplate.setConnectionFactory(connectionFactory); 14 StringRedisSerializer stringRedisSerializer = new StringRedisSerializer(); 15 redisTemplate.setKeySerializer(stringRedisSerializer); 16 redisTemplate.setHashKeySerializer(stringRedisSerializer); 17 Jackson2JsonRedisSerializer<?> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class); 18 redisTemplate.setValueSerializer(jackson2JsonRedisSerializer); 19 redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer); 20 redisTemplate.afterPropertiesSet(); 21 return redisTemplate; 22 } 23 24 @Bean 25 public TokenStore tokenStore(){ 26 return new RedisTokenStore(redisConnectionFactory); 27 } 28}

token配置类,将一些用户信息编入token。

1@Component 2public class JwtTokenEnhancer implements TokenEnhancer { 3 @Override 4 public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) { 5 if (authentication.isClientOnly()) { 6 return accessToken; 7 } 8 SecurityUser securityUser = (SecurityUser) authentication.getPrincipal(); 9 Map<String, Object> info = new HashMap<>(); 10 //把用户信息设置到JWT中 11 info.put("id", securityUser.getId()); 12 info.put("nickname", securityUser.getNickname()); 13 info.put("username", securityUser.getUsername()); 14 ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(info); 15 return accessToken; 16 } 17}

用户信息查询类,security通过该类来完成用户信息的查询。

1@Service 2@Slf4j 3@AllArgsConstructor 4public class UserServiceImpl implements UserDetailsService { 5 private List<UserDTO> userList; 6 private final IUserDao userDao; 7 private final IRoleDao roleDao; 8 private final IUserRoleDao userRoleDao; 9 private final PasswordEncoder passwordEncoder; 10 11 @PostConstruct 12 public void initData() { 13 log.info("初始化管理员账号信息"); 14 String password = passwordEncoder.encode("123456"); 15 userList = new ArrayList<>(); 16 userList.add(new UserDTO(10000L,"admin", "管理员", password,1, CollUtil.toList("ADMIN"))); 17 } 18 19 @Override 20 public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { 21 List<UserDTO> findUserList = userList.stream().filter(item -> item.getName().equals(username)).collect(Collectors.toList()); 22 23 if (CollUtil.isEmpty(findUserList)) { 24 Optional<UserDO> opt = userDao.findByUsername(username); 25 if (!opt.isPresent()){ 26 throw new UsernameNotFoundException(MessageConstant.USERNAME_PASSWORD_ERROR); 27 } 28 UserDO user = opt.get(); 29 30 Set<GrantedAuthority> grantedAuthorities = userRoleDao.findByUserId(user.getId()).stream() 31 .map(role -> new SimpleGrantedAuthority(roleDao.findById(role.getRoleId()).orElse(new RoleDO(0L, SecurityConstant.ROLE_ANONYMOUS)).getRoleName())) 32 .collect(Collectors.toSet()); 33 UserDTO userDTO = new UserDTO(user.getId(), user.getUsername(), user.getNickname(), user.getPassword(), user.getState(), grantedAuthorities.stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList())); 34 findUserList.add(userDTO); 35 } 36 if (CollUtil.isEmpty(findUserList)) { 37 throw new UsernameNotFoundException(MessageConstant.USERNAME_PASSWORD_ERROR); 38 } 39 SecurityUser securityUser = new SecurityUser(findUserList.get(0)); 40 if (!securityUser.isEnabled()) { 41 throw new DisabledException(MessageConstant.ACCOUNT_DISABLED); 42 } else if (!securityUser.isAccountNonLocked()) { 43 throw new LockedException(MessageConstant.ACCOUNT_LOCKED); 44 } else if (!securityUser.isAccountNonExpired()) { 45 throw new AccountExpiredException(MessageConstant.ACCOUNT_EXPIRED); 46 } else if (!securityUser.isCredentialsNonExpired()) { 47 throw new CredentialsExpiredException(MessageConstant.CREDENTIALS_EXPIRED); 48 } 49 return securityUser; 50 } 51 52}

在这个类中我做了个初始化的工作,定义了一个管理员。当然,这个操作不是必须得,管理员用户通常也是存储在数据库中的。loadUserByUsername方法会先去查内存中存储的用户,找不到再去数据库中查找。

资源访问权限初始化定义,该类中初始化了一些管理员可以访问的资源。

1@Service 2@Slf4j 3public class ResourceServiceImpl { 4 private final RedisTemplate<String,Object> redisTemplate; 5 6 public ResourceServiceImpl(RedisTemplate<String, Object> redisTemplate) { 7 this.redisTemplate = redisTemplate; 8 } 9 10 @PostConstruct 11 public void initData() { 12 log.info("初始化管理员权限"); 13 Map<String, List<String>> resourceRolesMap = new TreeMap<>(); 14 resourceRolesMap.put("/users", CollUtil.toList("ADMIN")); 15 redisTemplate.opsForHash().putAll(RedisConstant.RESOURCE_ROLES_MAP, resourceRolesMap); 16 } 17 18}

认证相关配置类:

1@AllArgsConstructor 2@Configuration 3@EnableAuthorizationServer 4public class Oauth2ServerConfig extends AuthorizationServerConfigurerAdapter { 5 private final PasswordEncoder passwordEncoder; 6 private final UserServiceImpl userDetailsService; 7 private final AuthenticationManager authenticationManager; 8 private final JwtTokenEnhancer jwtTokenEnhancer; 9 private final TokenStore tokenStore; 10 private final DataSource dataSource; 11 private final String pwd = "生成rsa证书时使用的密码"; 12 13 @Override 14 public void configure(ClientDetailsServiceConfigurer clients) throws Exception { 15 clients.jdbc(dataSource).passwordEncoder(passwordEncoder); 16 } 17 18 @Override 19 public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { 20 TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain(); 21 tokenEnhancerChain.setTokenEnhancers(Arrays.asList(jwtTokenEnhancer, accessTokenConverter())); 22 endpoints 23 //密码模式 24 .authenticationManager(authenticationManager) 25 //授权码服务 26 .authorizationCodeServices(authorizationCodeServices()) 27 //配置加载用户信息的服务 28 .userDetailsService(userDetailsService) 29 .tokenEnhancer(tokenEnhancerChain) 30// //配置为redis存储token 31 .tokenStore(tokenStore); 32 } 33 34 @Override 35 public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { 36 security 37 //oauth/token_key公开 38 .tokenKeyAccess("permitAll()") 39 //oauth/check_token公开 40 .checkTokenAccess("permitAll()") 41 .allowFormAuthenticationForClients(); 42 } 43 44 @Bean 45 public JwtAccessTokenConverter accessTokenConverter() { 46 JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter(); 47 accessTokenConverter.setKeyPair(keyPair()); 48 return accessTokenConverter; 49 } 50 51 @Bean 52 public KeyPair keyPair() { 53 //从classpath下的证书中获取秘钥对 54 KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), pwd.toCharArray()); 55 return keyStoreKeyFactory.getKeyPair("jwt", pwd.toCharArray()); 56 } 57 58 @Bean 59 public AuthorizationCodeServices authorizationCodeServices() { 60 //设置授权码模式的授权码如何存取 61 return new JdbcAuthorizationCodeServices(dataSource); 62 } 63 64}

@EnableAuthorizationServer注解表示该程序为认证服务器。

web安全配置类

1@Configuration 2@EnableWebSecurity 3public class WebSecurityConfig extends WebSecurityConfigurerAdapter { 4 private final UserLogoutSuccessHandler userLogoutSuccessHandler; 5 6 public WebSecurityConfig(UserLogoutSuccessHandler userLogoutSuccessHandler) { 7 this.userLogoutSuccessHandler = userLogoutSuccessHandler; 8 } 9 10 @Bean 11 @Override 12 public AuthenticationManager authenticationManagerBean() throws Exception { 13 return super.authenticationManagerBean(); 14 } 15 16 @Override 17 protected void configure(HttpSecurity http) throws Exception { 18 http.formLogin().permitAll().and() 19 .authorizeRequests() 20 .requestMatchers(EndpointRequest.toAnyEndpoint()).permitAll() 21 .antMatchers("/auth/rsa/publicKey").permitAll() 22 .and().logout().logoutSuccessHandler(userLogoutSuccessHandler) 23 .and().csrf().disable(); 24 } 25 @Bean 26 public PasswordEncoder passwordEncoder() { 27 return new BCryptPasswordEncoder(); 28 } 29 30 @Override 31 public void configure(WebSecurity web) { 32 web.ignoring().antMatchers( "/swagger-ui.html", 33 "/swagger-ui/*", 34 "/swagger-resources/**", 35 "/v2/api-docs", 36 "/v3/api-docs", 37 "/webjars/**"); 38 } 39}

登录接口

security oauth提供了默认的登录接口/oauth/token。

我们对它进行重写,主要是修改返回的信息。

1@RestController 2@RequestMapping("/oauth") 3@Api(tags = "登录") 4public class AuthController { 5 private final TokenEndpoint tokenEndpoint; 6 7 public AuthController(TokenEndpoint tokenEndpoint) { 8 this.tokenEndpoint = tokenEndpoint; 9 } 10 11 /** 12 * Oauth2登录认证 13 */ 14 @RequestMapping(value = "/token", method=RequestMethod.POST) 15 @ApiOperation(value = "登录") 16 public ResultJson<Auth2TokenVo> postAccessToken(Principal principal, @RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException { 17 OAuth2AccessToken oAuth2AccessToken = tokenEndpoint.postAccessToken(principal, parameters).getBody(); 18 Auth2TokenVo auth2Token = Auth2TokenVo.builder() 19 .token(oAuth2AccessToken.getValue()) 20 .refreshToken(oAuth2AccessToken.getRefreshToken() != null ? oAuth2AccessToken.getRefreshToken().getValue() : null) 21 .expiresIn(oAuth2AccessToken.getExpiresIn()) 22 .tokenHead("Bearer ").build(); 23 return ResultJson.ok(auth2Token); 24 } 25}

获取公钥接口,资源服务器会获取公钥并对token进行校验。

1@RestController 2@RequestMapping("/auth") 3@Api(tags = "公钥") 4public class KeyPairController { 5 6 private final KeyPair keyPair; 7 8 public KeyPairController(KeyPair keyPair) { 9 this.keyPair = keyPair; 10 } 11 12 @GetMapping("/rsa/publicKey") 13 @ApiOperation(value = "获取公钥") 14 public Map<String, Object> getKey() { 15 RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); 16 RSAKey key = new RSAKey.Builder(publicKey).build(); 17 return new JWKSet(key).toJSONObject(); 18 } 19 20}

最后在启动类上增加注解:@EnableDiscoveryClient

以上只提供了关键代码。

全部代码请参考github仓库:

GitHub - pipijoe/oauth-service

2023年04月22日
在初学者眼中,世界充满了可能;专家眼中,世界大都已经既定。--铃木俊隆