Spring Security + OAuth2 - 黑马程序员(7. Spring Security实现分布式系统授权【从头重写】- UAA)学习笔记

上一篇:Spring Security + OAuth2(6. JWT 令牌)

下一篇:Spring Security + OAuth2 (8. Spring Security实现分布式系统授权【从头重写】- Gateway-Order)

提示:这一部分使用到的组件和原视频不太一样,所以有很多不一样的地方
  • 注册中心 :Naocs
  • 网关:Gateway
  • 因为这两个组件不一样,所以很多配置、过滤器也不一样
  • 如果是想看和原视频一样的代码,那请找其他文章把。
  • 还有就是我也是初学者,有很多落地的东西我也理解的不是很到位,如果有写错的、写的不好的地方欢迎指正。
  • 在这里很多不重要的就不多说了,下面是我的代码地址:https://gitee.com/yuan934672344/demo-spring-security

1. 需求分析

技术方案如下:
在这里插入图片描述
说明:

  1. UAA认证服务负责认证授权。
  2. 所有请求经过 网关到达微服务
  3. 网关负责鉴权客户端以及请求转发
  4. 网关将token解析后传给微服务,微服务进行授权。

2. 编写公共模块

新建 Maven 项目: commons-api
具体内容 略,详情请看代码。
其中包括:

  • 配置文件
  1. Redis 配置文件:RedisConfig
  • 实体类,entity
  1. 编写用户实体类:User
  2. 资源实体类: Resource
  3. 角色实体类: Role
  4. 资源-角色关系实体类:RoleResourceRel
  • 上述实体类对应的 Mapper、Service 文件
  • 工具类 utils
  1. 统一的返回对象:Result
  2. 统一的返回的消息内容:MessageConstant
  3. Redis 工具类:RedisUtil
  4. 对对象等进行判断的工具类:UtilValidate

3. 重新编写 UAA 模块

  • 为和之前区别,包名和之前有所不同
  • 新建 Maven 项目 :uaa-server
  • 文件目录
    在这里插入图片描述

3.1. 修改 POM 文件

  • 略……

3.2. 编写配置文件

  • 因为我有使用到 Nacos Config,所以把配置文件拆分了。
  1. bootstrap.properties
  2. application.yml

3.3. 编写主启动类

  1. UaaMain

3.4. 编写业务类

1. 编写需要用到的实体类 DTO

  1. Oauth2TokenDto,用于封装令牌的相关信息

    @Data
    @EqualsAndHashCode(callSuper = false)
    @Builder
    public class Oauth2TokenDto {
        /** 访问令牌 */
        private String token;
        /** 刷新令牌 */
        private String refreshToken;
        /** 访问令牌头前缀  */
        private String tokenHead;
        /** 有效时间(秒) */
        private int expiresIn;
    }
    
  2. 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. 配置令牌

  1. 配置 令牌的加密规则

    @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());
    	}
    }
    
  2. 配置令牌增强,扩展令牌的内容

    @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;
    	}
    }
    
  3. 配置 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. 编写对外的接口

  1. 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();
        }
    }
    
  2. 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 模块

  1. 启动 UAA 模块
  2. 启动 PostMan,我是使用 PostMan 进行测试
  3. 访问 :http://localhost:8090/oauth/token?grant_type=password&client_secret=secret&password=123456&username=11111user&client_id=myjob
    在这里插入图片描述
  4. 检验令牌
    在这里插入图片描述
  5. 测试通过,UAA 模块编写完成
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

yuan_404

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值