上一篇:Spring Security + OAuth2(6. JWT 令牌)
下一篇:Spring Security + OAuth2 (8. Spring Security实现分布式系统授权【从头重写】- Gateway-Order)
文章目录
提示:这一部分使用到的组件和原视频不太一样,所以有很多不一样的地方
- 注册中心 :Naocs
- 网关:Gateway
- 因为这两个组件不一样,所以很多配置、过滤器也不一样
- 如果是想看和原视频一样的代码,那请找其他文章把。
- 还有就是我也是初学者,有很多落地的东西我也理解的不是很到位,如果有写错的、写的不好的地方欢迎指正。
- 在这里很多不重要的就不多说了,下面是我的代码地址:https://gitee.com/yuan934672344/demo-spring-security
1. 需求分析
技术方案如下:
说明:
- UAA认证服务负责认证授权。
- 所有请求经过 网关到达微服务
- 网关负责鉴权客户端以及请求转发
- 网关将token解析后传给微服务,微服务进行授权。
2. 编写公共模块
新建 Maven 项目: commons-api
具体内容 略,详情请看代码。
其中包括:
- 配置文件
- Redis 配置文件:RedisConfig
- 实体类,entity
- 编写用户实体类:User
- 资源实体类: Resource
- 角色实体类: Role
- 资源-角色关系实体类:RoleResourceRel
- 上述实体类对应的 Mapper、Service 文件
- 工具类 utils
- 统一的返回对象:Result
- 统一的返回的消息内容:MessageConstant
- Redis 工具类:RedisUtil
- 对对象等进行判断的工具类:UtilValidate
3. 重新编写 UAA 模块
- 为和之前区别,包名和之前有所不同
- 新建 Maven 项目 :uaa-server
- 文件目录
3.1. 修改 POM 文件
- 略……
3.2. 编写配置文件
- 因为我有使用到 Nacos Config,所以把配置文件拆分了。
- bootstrap.properties
- application.yml
3.3. 编写主启动类
- UaaMain
3.4. 编写业务类
1. 编写需要用到的实体类 DTO
-
Oauth2TokenDto,用于封装令牌的相关信息
@Data @EqualsAndHashCode(callSuper = false) @Builder public class Oauth2TokenDto { /** 访问令牌 */ private String token; /** 刷新令牌 */ private String refreshToken; /** 访问令牌头前缀 */ private String tokenHead; /** 有效时间(秒) */ private int expiresIn; }
-
SecurityUser,实现
UserDetails
@Data @NoArgsConstructor public class SecurityUser implements UserDetails { /** 这里的字段可以按照自己要求自定义,后面可以将这些信息存入 JWT 令牌 */ /** ID */ private Long id; /** 用户名 */ private String username; /** 用户密码 */ private String password; /** 用户状态 */ private Boolean enabled = true; /** 权限数据 */ private Collection<SimpleGrantedAuthority> authorities; // 构造方法 public SecurityUser(User user) { this.setId(Long.valueOf(user.getUserId())); this.setUsername(user.getUserName()); this.setPassword(user.getPassword()); /* authorities 本应该是插入权限信息的,但是权限信息太多了, 如果都放进 JWT 令牌的话,生成的 JWT 令牌就会过于长,也会占用很多空间。*/ /* 所以这里传入角色信息,再将角色对应的权限信息存入 Redis, 后面通过该角色信息,从 Redis 中再查询出权限信息,进而进行鉴权操作。 */ /* 当然这里说的都是简单情况,如果需要对每一个用户进行单独的分配权限的话,就不能这样了*/ if (user.getRole() != null) { authorities = new ArrayList<>(); authorities.add(new SimpleGrantedAuthority("["+user.getRoleCode()+"]")); } } /** 下面是实现 UserDetails 接口中的一些方法 */ @Override public Collection<? extends GrantedAuthority> getAuthorities() { return this.authorities; } @Override public String getPassword() { return this.password; } @Override public String getUsername() { return this.username; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return this.enabled; } }
2. 编写 WebSecurityConfig,进行安全配置
- WebSecurityConfig
@Configuration @EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { /** * 认证管理器 */ @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } /** * 密码编码器 */ @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } /** * 安全拦截机制(最重要) */ @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .authorizeRequests() .requestMatchers(EndpointRequest.toAnyEndpoint()).permitAll() .antMatchers("/rsa/publicKey").permitAll() .anyRequest().authenticated() .and() .formLogin(); } }
3. 配置令牌
-
配置 令牌的加密规则
@Configuration public class TokenConfig { //配置 JWT 令牌存储方案 @Bean public TokenStore tokenStore() { return new JwtTokenStore(accessTokenConverter()); } // 配置令牌的加密规则 @Bean public JwtAccessTokenConverter accessTokenConverter() { JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter(); jwtAccessTokenConverter.setKeyPair(keyPair()); return jwtAccessTokenConverter; } //从classpath下的证书中获取秘钥对 @Bean public KeyPair keyPair() { org.springframework.security.rsa.crypto.KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), "123456".toCharArray()); return keyStoreKeyFactory.getKeyPair("jwt", "123456".toCharArray()); } }
-
配置令牌增强,扩展令牌的内容
@Component public class JwtTokenEnhancer implements TokenEnhancer { @Override public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) { SecurityUser user = (SecurityUser) authentication.getPrincipal(); Map<String, Object> info = new HashMap<>(); //把用户ID设置到JWT中 info.put("id", user.getId()); ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(info); return accessToken; } }
-
配置 OAuth 的相关设置
@Configuration @EnableAuthorizationServer public class AuthorizationServer extends AuthorizationServerConfigurerAdapter { @Autowired private TokenStore tokenStore; @Autowired private ClientDetailsService clientDetailsService; @Autowired private AuthorizationCodeServices authorizationCodeServices; @Autowired private AuthenticationManager authenticationManager; @Autowired private JwtAccessTokenConverter accessTokenConverter; @Autowired private PasswordEncoder passwordEncoder; @Autowired private JwtTokenEnhancer jwtTokenEnhancer; //通过数据库存取用户信息 @Bean public ClientDetailsService clientDetailsService(DataSource dataSource) { JdbcClientDetailsService clientDetailsService = new JdbcClientDetailsService(dataSource); clientDetailsService.setPasswordEncoder(passwordEncoder); return clientDetailsService; } // 客户端详情服务,也就是配置支持哪些客户端来请求 // 这里是配置从数据库获取信息 @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.withClientDetails(clientDetailsService); } // 配置授权相关的服务 // 用来配置令牌(token)的访问端点 和 管理规则 @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) { endpoints //认证管理器 .authenticationManager(authenticationManager) //授权码服务 .authorizationCodeServices(authorizationCodeServices) //令牌管理服务 .tokenServices(tokenService()) .allowedTokenEndpointRequestMethods(HttpMethod.POST); } // 用来配置令牌端点的安全约束 @Override public void configure(AuthorizationServerSecurityConfigurer security){ security //oauth/token_key是公开 .tokenKeyAccess("permitAll()") //oauth/check_token公开 .checkTokenAccess("permitAll()") //表单认证(申请令牌) .allowFormAuthenticationForClients(); } /** * 令牌管理服务相关配置,以及令牌信息的增强 */ @Bean public AuthorizationServerTokenServices tokenService() { DefaultTokenServices service=new DefaultTokenServices(); //支持刷新令牌 service.setSupportRefreshToken(true); //令牌存储策略 service.setTokenStore(tokenStore); //令牌增强 TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain(); List<TokenEnhancer> delegates = new ArrayList<>(); delegates.add(jwtTokenEnhancer); delegates.add(accessTokenConverter); tokenEnhancerChain.setTokenEnhancers(delegates); service.setTokenEnhancer(tokenEnhancerChain); // 令牌默认有效期2小时 service.setAccessTokenValiditySeconds(7200); // 刷新令牌默认有效期3天 service.setRefreshTokenValiditySeconds(259200); return service; } /** * 设置授权码模式生成的授权码存入数据库 * @param dataSource 数据源 */ @Bean public AuthorizationCodeServices authorizationCodeServices(DataSource dataSource) { //设置授权码模式的授权码如何存取 return new JdbcAuthorizationCodeServices(dataSource); } }
4. 编写用户的密码角色的查询
-
SpringDataUserDetailsServiceImpl, 实现
UserDetailsService
@Service @Slf4j public class SpringDataUserDetailsServiceImpl implements UserDetailsService { @Autowired private ResourceService resourceService; @Autowired private RoleResourceRelService roleResourceRelService; @Autowired private RoleService roleService; @Autowired private UserService userService; @Autowired private RedisUtil redisUtil; @Autowired private PasswordEncoder passwordEncoder; //根据 账号查询用户信息 @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { Boolean isEmailCode = false; if (username.contains("##")) { username = username.replace("##","").trim(); isEmailCode = true; } //通过传来的用户名查询用户信息 User user = this.getUserByUsername(username); if(UtilValidate.isEmpty(user)){ log.info("<< AUTH >> --- 传来的 UserName:"+username+" , 查无此人,抛出异常"); //如果用户查不到,返回null,由provider来抛出异常 throw new UsernameNotFoundException(MessageConstant.USERNAME_PASSWORD_ERROR); } log.info("<< AUTH >> --- 传来的 UserName:"+username+" , 查询到的结果为:"+user.toString()); this.selectAllRoleResourceRel(); this.getRoleCode(user); if (isEmailCode){ String password = user.getPassword(); password = passwordEncoder.encode(password); user.setPassword(password); } return new SecurityUser(user); } /** * 根据账号查询用户信息 */ public User getUserByUsername(String username){ User user = userService.selectByUserName(username); if (UtilValidate.isNotEmpty(user)) { return user; } return null; } public void selectAllRoleResourceRel(){ // 查询出所有的资源信息 List<RoleResourceRel> roleResources = roleResourceRelService.list(); List<Resource> resources = resourceService.list(); HashMap<String, Resource> resourceMap = new HashMap<>(); for (Resource resource : resources) { resourceMap.put(String.valueOf(resource.getResourceId()), resource); } // 将 resourceUrl 和 roleId 对应关系存进 Redis setUrlAndRoleIdsRelToRedis(roleResources, resourceMap); // 将 roleId 和 resourceCode 对应关系存进 Redis setRoleIdAndResourceCodeToRedis(roleResources, resourceMap); } public void getRoleCode(User user){ if (UtilValidate.isEmpty(user)) { return; } QueryWrapper<Role> wrapper = new QueryWrapper<>(); wrapper.eq("name", user.getRole()); Role one = roleService.getOne(wrapper); if (UtilValidate.isEmpty(one)) { return; }else { user.setRoleCode(String.valueOf(one.getId())); } } //@Async public Boolean setUrlAndRoleIdsRelToRedis(List<RoleResourceRel> roleResources, HashMap<String, Resource> resourceMap){ Map<Object, Object> roleResourceRel = redisUtil.hmget(Constants.KEY_IN_REDIS.AUTH_RESOURCE_URL_ROLE_IDS_CEL.getValue()); if (UtilValidate.isNotEmpty(roleResourceRel)) { log.info("<< AUTH >> --- resourceUrl 和 roleId 关系信息已经存在直接返回"); return true; } HashMap<String, List<String>> roleResourceMap = new HashMap<>(); for (RoleResourceRel roleResource : roleResources) { Long resourceId = roleResource.getResourceId(); Resource resource = resourceMap.get(String.valueOf(resourceId)); String key = resource.getResourceMethod() + "-" + resource.getResourceUrl(); List<String> list = roleResourceMap.get(key); if (UtilValidate.isEmpty(list)){ list = new ArrayList<>(); } list.add("["+roleResource.getRoleId()+"]"); roleResourceMap.put(key, list); } log.info("<< AUTH >> --- 正在将 (资源URL, 角色ID) 关系信息存入 Redis"); boolean hmset = redisUtil.hmset(Constants.KEY_IN_REDIS.AUTH_RESOURCE_URL_ROLE_IDS_CEL.getValue(), roleResourceMap, 12, TimeUnit.HOURS); if (!hmset){ // 不成功重试一次 log.info("<< AUTH >> --- 出错, 进行重试"); hmset = redisUtil.hmset(Constants.KEY_IN_REDIS.AUTH_RESOURCE_URL_ROLE_IDS_CEL.getValue(), roleResourceMap, 12, TimeUnit.HOURS); } log.info("<< AUTH >> --- 成功将 (资源URL, 角色ID) 关系信息存入 Redis"); return hmset; } //@Async public Boolean setRoleIdAndResourceCodeToRedis(List<RoleResourceRel> roleResources, HashMap<String, Resource> resourceMap){ Map<Object, Object> roleResourceRel = redisUtil.hmget(Constants.KEY_IN_REDIS.AUTH_ROLE_ID_RESOURCE_CODE_CEL.getValue()); if (UtilValidate.isNotEmpty(roleResourceRel)) { log.info("<< AUTH >> --- roleId 和 resourceCode 关系信息已经存在直接返回"); return true; } HashMap<String, List<String>> roleResourceMap = new HashMap<>(); for (RoleResourceRel roleResource : roleResources) { String key = String.valueOf(roleResource.getRoleId()); Long resourceId = roleResource.getResourceId(); Resource resource = resourceMap.get(String.valueOf(resourceId)); List<String> resources = roleResourceMap.get(key); if (UtilValidate.isEmpty(resources)) { resources = new ArrayList<>(); } resources.add(resource.getResourceCode()); roleResourceMap.put(key, resources); } log.info("<< AUTH >> --- 正在将 (角色ID, 资源Code) 关系信息存入 Redis"); boolean hmset = redisUtil.hmset(Constants.KEY_IN_REDIS.AUTH_ROLE_ID_RESOURCE_CODE_CEL.getValue(), roleResourceMap, 12, TimeUnit.HOURS); if (!hmset){ // 不成功重试一次 log.info("<< AUTH >> --- 出错, 进行重试"); hmset = redisUtil.hmset(Constants.KEY_IN_REDIS.AUTH_ROLE_ID_RESOURCE_CODE_CEL.getValue(), roleResourceMap, 12, TimeUnit.HOURS); } log.info("<< AUTH >> --- 成功将 (角色ID, 资源Code) 关系信息存入 Redis"); return hmset; } }
5. 编写对外的接口
-
KeyPairController, 编写获取 JWT 令牌加密密钥的接口
@RestController public class KeyPairController { @Autowired private KeyPair keyPair; @GetMapping("/rsa/publicKey") public Map<String, Object> getKey() { RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); RSAKey key = new RSAKey.Builder(publicKey).build(); return new JWKSet(key).toJSONObject(); } }
-
AuthController, 封装获取令牌的接口
@RestController @RequestMapping("/oauth") @Slf4j public class AuthController { @Autowired private TokenEndpoint tokenEndpoint; /** * Oauth2登录认证 */ @PostMapping(value = "/token") public String postAccessToken(Principal principal, @RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException { OAuth2AccessToken oAuth2AccessToken = tokenEndpoint.postAccessToken(principal, parameters).getBody(); Oauth2TokenDto oauth2TokenDto = Oauth2TokenDto.builder() .token(oAuth2AccessToken.getValue()) .refreshToken(oAuth2AccessToken.getRefreshToken().getValue()) .expiresIn(oAuth2AccessToken.getExpiresIn()) .tokenHead("Bearer ").build(); Result<Oauth2TokenDto> result = Result.succeed(oauth2TokenDto); return JSONObject.toJSONString(result); } }
4. 测试 UAA 模块
- 启动 UAA 模块
- 启动 PostMan,我是使用 PostMan 进行测试
- 访问 :http://localhost:8090/oauth/token?grant_type=password&client_secret=secret&password=123456&username=11111user&client_id=myjob
- 检验令牌
- 测试通过,UAA 模块编写完成