本文主要是介绍Spring Security + OAuth2 - 黑马程序员(7. Spring Security实现分布式系统授权【从头重写】- UAA)学习笔记,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
上一篇:Spring Security + OAuth2(6. JWT 令牌)
下一篇:Spring Security + OAuth2 (8. Spring Security实现分布式系统授权【从头重写】- Gateway-Order)
文章目录
- 提示:这一部分使用到的组件和原视频不太一样,所以有很多不一样的地方
- 1. 需求分析
- 2. 编写公共模块
- 3. 重新编写 UAA 模块
- 3.1. 修改 POM 文件
- 3.2. 编写配置文件
- 3.3. 编写主启动类
- 3.4. 编写业务类
- 1. 编写需要用到的实体类 DTO
- 2. 编写 WebSecurityConfig,进行安全配置
- 3. 配置令牌
- 4. 编写用户的密码角色的查询
- 5. 编写对外的接口
- 4. 测试 UAA 模块
提示:这一部分使用到的组件和原视频不太一样,所以有很多不一样的地方
- 注册中心 :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 接口中的一些方法 */@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return this.authorities;}@Overridepublic String getPassword() {return this.password;}@Overridepublic String getUsername() {return this.username;}@Overridepublic boolean isAccountNonExpired() {return true;}@Overridepublic boolean isAccountNonLocked() {return true;}@Overridepublic boolean isCredentialsNonExpired() {return true;}@Overridepublic boolean isEnabled() {return this.enabled;} }
2. 编写 WebSecurityConfig,进行安全配置
- WebSecurityConfig
@Configuration @EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter {/*** 认证管理器*/@Bean@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}/*** 密码编码器*/@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}/*** 安全拦截机制(最重要)*/@Overrideprotected 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 令牌存储方案@Beanpublic TokenStore tokenStore() {return new JwtTokenStore(accessTokenConverter());}// 配置令牌的加密规则@Beanpublic JwtAccessTokenConverter accessTokenConverter() {JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();jwtAccessTokenConverter.setKeyPair(keyPair());return jwtAccessTokenConverter;}//从classpath下的证书中获取秘钥对@Beanpublic 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 {@Overridepublic 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 {@Autowiredprivate TokenStore tokenStore;@Autowiredprivate ClientDetailsService clientDetailsService;@Autowiredprivate AuthorizationCodeServices authorizationCodeServices;@Autowiredprivate AuthenticationManager authenticationManager;@Autowiredprivate JwtAccessTokenConverter accessTokenConverter;@Autowiredprivate PasswordEncoder passwordEncoder;@Autowiredprivate JwtTokenEnhancer jwtTokenEnhancer;//通过数据库存取用户信息@Beanpublic ClientDetailsService clientDetailsService(DataSource dataSource) {JdbcClientDetailsService clientDetailsService = new JdbcClientDetailsService(dataSource);clientDetailsService.setPasswordEncoder(passwordEncoder);return clientDetailsService;}// 客户端详情服务,也就是配置支持哪些客户端来请求// 这里是配置从数据库获取信息@Overridepublic void configure(ClientDetailsServiceConfigurer clients) throws Exception {clients.withClientDetails(clientDetailsService);}// 配置授权相关的服务// 用来配置令牌(token)的访问端点 和 管理规则@Overridepublic void configure(AuthorizationServerEndpointsConfigurer endpoints) {endpoints//认证管理器.authenticationManager(authenticationManager)//授权码服务.authorizationCodeServices(authorizationCodeServices)//令牌管理服务.tokenServices(tokenService()).allowedTokenEndpointRequestMethods(HttpMethod.POST);}// 用来配置令牌端点的安全约束@Overridepublic void configure(AuthorizationServerSecurityConfigurer security){security//oauth/token_key是公开.tokenKeyAccess("permitAll()")//oauth/check_token公开.checkTokenAccess("permitAll()")//表单认证(申请令牌).allowFormAuthenticationForClients();}/*** 令牌管理服务相关配置,以及令牌信息的增强*/@Beanpublic 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 数据源*/@Beanpublic AuthorizationCodeServices authorizationCodeServices(DataSource dataSource) {//设置授权码模式的授权码如何存取return new JdbcAuthorizationCodeServices(dataSource);} }
4. 编写用户的密码角色的查询
-
SpringDataUserDetailsServiceImpl, 实现
UserDetailsService
@Service @Slf4j public class SpringDataUserDetailsServiceImpl implements UserDetailsService {@Autowiredprivate ResourceService resourceService;@Autowiredprivate RoleResourceRelService roleResourceRelService;@Autowiredprivate RoleService roleService;@Autowiredprivate UserService userService;@Autowiredprivate RedisUtil redisUtil;@Autowiredprivate PasswordEncoder passwordEncoder;//根据 账号查询用户信息@Overridepublic 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 对应关系存进 RedissetUrlAndRoleIdsRelToRedis(roleResources, resourceMap);// 将 roleId 和 resourceCode 对应关系存进 RedissetRoleIdAndResourceCodeToRedis(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()));}}//@Asyncpublic 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;}//@Asyncpublic 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 {@Autowiredprivate 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 {@Autowiredprivate 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 模块编写完成
这篇关于Spring Security + OAuth2 - 黑马程序员(7. Spring Security实现分布式系统授权【从头重写】- UAA)学习笔记的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!