前言添加引用先获取一下Redis的配置创建一个TokenGranterConfig,重新配置一下授权设置继承AbstractTokenGranter ,实现自定义的密码认证实现UserDetailsService 获取用户信息的具体逻辑继承AuthorizationServerConfigurerAdapter 进行授权/认证服务器的配置继承WebSecurityConfigurerAdapter类,复写方法实现自定义安全访问策略关于服务端,我们也要配置Oauth2,拦截请求必须带有认证信息且认证有效才能访问接口最后
前言微服务中使用Oauth2做授权认证,想要实现以下几点
1、单点登录,所以首先要将认证信息都存储在redis中
2、针对用户名密码方式获取授权,添加更多的细节 *** 作,下面实现的细节只是一些简单的例子
先获取一下Redis的配置org.springframework.cloud >spring-cloud-starter-oauth2org.springframework.boot >spring-boot-starter-data-redis
简单配置一下redis
custom: datasource: redis: ip: 127.0.0.1 port: 6379 smsExpire: 60000
@Data @ConfigurationProperties(prefix = "custom.datasource.redis") public class RedisProperties { private String ip; private int port; private int smsExpire; }创建一个TokenGranterConfig,重新配置一下授权设置
@Configuration public class TokenGranterConfig { //客户端认证 //在后面的相关配置中 配置了从数据库中读取,也可以存在内存中 InMemery //在后面的AuthorizationServerConfig中有相关配置 数据库默认 client_id:app client_secret:加密(app) //标志在调用 oauth/token 获取授权时,前端需要传递 client_id:app 和 client_secret:app @Autowired private ClientDetailsService clientDetailsService; //Token授权方式 private TokenGranter tokenGranter; //Token存储 @Autowired private TokenStore tokenStore; // 认证管理器 用于处理一个认证请求,也就是Spring Security中的Authentication认证令牌。 @Autowired private AuthenticationManager authenticationManager; private AuthorizationServerTokenServices tokenServices; private boolean reuseRefreshToken = true; private AuthorizationCodeServices authorizationCodeServices; @Autowired private UserDetailsService userDetailsService; //注册 TokenGranter的Bean,后面在配置授权认证服务器时候会注入这个Bean @Bean public TokenGranter tokenGranter(){ if(null == tokenGranter){ tokenGranter = new TokenGranter() { private CompositeTokenGranter delegate; @Override public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) { if(delegate == null){ //返回一个复合的认证机制 delegate = new CompositeTokenGranter(getDefaultTokenGranters()); } return delegate.grant(grantType,tokenRequest); } }; } return tokenGranter; } //支持的授权模式 private List继承AbstractTokenGranter ,实现自定义的密码认证getDefaultTokenGranters() { AuthorizationServerTokenServices tokenServices = tokenServices(); AuthorizationCodeServices authorizationCodeServices = authorizationCodeServices(); OAuth2RequestFactory requestFactory = requestFactory(); List tokenGranters = new ArrayList(); //四种默认的授权模式 //授权码模式 tokenGranters.add(new AuthorizationCodeTokenGranter(tokenServices, authorizationCodeServices, clientDetailsService, requestFactory)); //refresh模式 tokenGranters.add(new RefreshTokenGranter(tokenServices, clientDetailsService, requestFactory)); //简化模式 ImplicitTokenGranter implicit = new ImplicitTokenGranter(tokenServices, clientDetailsService, requestFactory); tokenGranters.add(implicit); //客户端模式 tokenGranters.add(new ClientCredentialsTokenGranter(tokenServices, clientDetailsService, requestFactory)); if (authenticationManager != null) { //自定义的密码模式 tokenGranters.add(new CustomResourceOwnerPasswordTokenGranter(authenticationManager, tokenServices, clientDetailsService, requestFactory)); } return tokenGranters; } private AuthorizationServerTokenServices tokenServices() { if (tokenServices != null) { return tokenServices; } this.tokenServices = createDefaultTokenServices(); return tokenServices; } private AuthorizationServerTokenServices createDefaultTokenServices() { DefaultTokenServices tokenServices = new DefaultTokenServices(); tokenServices.setTokenStore(tokenStore); tokenServices.setSupportRefreshToken(true); tokenServices.setReuseRefreshToken(reuseRefreshToken); tokenServices.setClientDetailsService(clientDetailsService); addUserDetailsService(tokenServices, this.userDetailsService); return tokenServices; } private void addUserDetailsService(DefaultTokenServices tokenServices, UserDetailsService userDetailsService) { if (userDetailsService != null) { PreAuthenticatedAuthenticationProvider provider = new PreAuthenticatedAuthenticationProvider(); provider.setPreAuthenticatedUserDetailsService(new UserDetailsByNameServiceWrapper (userDetailsService)); tokenServices.setAuthenticationManager(new ProviderManager(Arrays.asList(provider))); } } private OAuth2RequestFactory requestFactory() { return new DefaultOAuth2RequestFactory(clientDetailsService); } private AuthorizationCodeServices authorizationCodeServices() { if (this.authorizationCodeServices == null) { this.authorizationCodeServices = new InMemoryAuthorizationCodeServices(); } return this.authorizationCodeServices; } }
模拟ResourceOwnerPasswordTokenGranter类,主要是针对密码验证过程中出现的异常,采取不同的策略
@Slf4j public class CustomResourceOwnerPasswordTokenGranter extends AbstractTokenGranter { private UserAccountDao userAccountDao; //认证模式 在前端传递 grant_code = password private static final String GRANT_TYPE = "password"; private final AuthenticationManager authenticationManager; public CustomResourceOwnerPasswordTokenGranter(AuthenticationManager authenticationManager, AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory) { this(authenticationManager, tokenServices, clientDetailsService, requestFactory, GRANT_TYPE); } protected CustomResourceOwnerPasswordTokenGranter(AuthenticationManager authenticationManager, AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory, String grantType) { super(tokenServices, clientDetailsService, requestFactory, grantType); this.authenticationManager = authenticationManager; } //主要实现方法 @Override protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) { Map实现UserDetailsService 获取用户信息的具体逻辑parameters = new linkedHashMap(tokenRequest.getRequestParameters()); String username = (String)parameters.get("username"); String password = (String)parameters.get("password"); parameters.remove("password"); Authentication userAuth = new UsernamePasswordAuthenticationToken(username, password); ((AbstractAuthenticationToken)userAuth).setDetails(parameters); userAccountDao = SpringContextUtils.getBean(UserAccountDao.class); try { //在这里会调用UserDetailServiceImpl里面的实现 userAuth = this.authenticationManager.authenticate(userAuth); //账户密码正确 lock_flag 重置为 0 UpdateWrapper udpa = new UpdateWrapper<>(); udpa.setSql("lock_flag = 0 "); udpa.eq("user_name",username); UserAccount updateDto = new UserAccount(); userAccountDao.update(updateDto, udpa); } catch (NonUsernameException var9) { throw new UsernameNotFoundException("用户不存在"); } catch (AccountStatusException var8) { throw new InvalidGrantException(var8.getMessage()); } catch (BadCredentialsException var10) { try { //账户密码错误 修改 lock_flag + 1 UpdateWrapper udpa = new UpdateWrapper<>(); udpa.setSql("lock_flag = lock_flag+1 "); udpa.eq("user_name",username); UserAccount updateDto = new UserAccount(); userAccountDao.update(updateDto, udpa); }catch (Exception ex91){ throw new RuntimeException("错误次数累加失败"); } throw new InvalidGrantException("账号密码错误,错误三次将锁定账户"); } if (userAuth != null && userAuth.isAuthenticated()) { OAuth2Request storedOAuth2Request = this.getRequestFactory().createOAuth2Request(client, tokenRequest); return new OAuth2Authentication(storedOAuth2Request, userAuth); } else { throw new InvalidGrantException("Could not authenticate user: " + username); } } }
这里主要是根据请求中的username获取数据库中加密后的密码,以便后面的逻辑进行密码匹配
具体逻辑根据实际场景实现
public class UserDetailServiceImpl implements UserDetailsService { @Autowired private UserAccountDao userDao; @Override public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException { //获取本地用户 QueryWrapper继承AuthorizationServerConfigurerAdapter 进行授权/认证服务器的配置userQueryWrapper =new QueryWrapper<>(); userQueryWrapper.eq("user_name",userName).eq("delete_flag",0); UserAccount user = userDao.selectOne(userQueryWrapper); if(user != null){ //判断锁定次数是否超过三次 int lockValue = null == user.getLockFlag()?0:user.getLockFlag(); if(lockValue>=3){ throw new LockedException("密码尝试超过三次,账户已被锁定!"); } UserDetails userr = User.builder() .username(user.getUserName()) .password(user.getPassword()) .authorities(AuthorityUtils.createAuthorityList("ADMIN")) .build(); return userr; }else{ throw new NonUsernameException("用户不存在"); } } }
@Configuration @EnableAuthorizationServer @EnableConfigurationProperties(RedisProperties.class) public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { @Autowired private UserDetailServiceImpl userDetailService; // 认证管理器 @Autowired private AuthenticationManager authenticationManager; @Autowired private DataSource dataSource; @Autowired private TokenGranter tokenGranter; private RedisProperties redisProperties; public AuthorizationServerConfig(RedisProperties redisProperties ){ this.redisProperties =redisProperties; } @Bean public TokenStore tokenStore() { //采用 Redis 存储 return new RedisTokenStore(redisConnectionFactory()); } @Bean public RedisConnectionFactory redisConnectionFactory(){ JedisPoolConfig poolConfig = new JedisPoolConfig(); poolConfig.setMaxTotal(100); poolConfig.setMaxIdle(50); poolConfig.setMaxWaitMillis(3000); poolConfig.setTestOnBorrow(true); poolConfig.setTestOnReturn(false); poolConfig.setTestWhileIdle(true); JedisClientConfiguration clientConfig = JedisClientConfiguration.builder() .usePooling().poolConfig(poolConfig).and().readTimeout(Duration.ofMillis(1000)).build(); // 单点redis RedisStandaloneConfiguration redisConfig = new RedisStandaloneConfiguration(); redisConfig.setHostName(redisProperties.getIp()); redisConfig.setPort(redisProperties.getPort()); return new JedisConnectionFactory(redisConfig,clientConfig); } @Bean public ClientDetailsService clientDetails() { return new JdbcClientDetailsService(dataSource); } @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } @Primary @Bean public DefaultTokenServices tokenServices(){ DefaultTokenServices tokenServices = new DefaultTokenServices(); tokenServices.setTokenStore(tokenStore()); //开启支持refresh_token,此处如果之前没有配置,启动服务后再配置重启服务,可能会导致不返回token的问题,解决方式:清除redis对应token存储 tokenServices.setSupportRefreshToken(true); //设置token有效期,默认12小时,此处修改为3小时 tokenServices.setAccessTokenValiditySeconds(60 * 60 * 3); //设置refresh_token的有效期,默认30天,此处修改为3天 tokenServices.setRefreshTokenValiditySeconds(60 * 60 * 24 * 3); return tokenServices; } @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { //自定义授权模式 endpoints.tokenGranter(tokenGranter); } @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { security.allowFormAuthenticationForClients() //如果使用表单认证则需要加上 .tokenKeyAccess("permitAll()") .checkTokenAccess("isAuthenticated()"); } @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.withClientDetails(clientDetails()); } }继承WebSecurityConfigurerAdapter类,复写方法实现自定义安全访问策略
比较重要的方法
@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { //自定义用户认证逻辑 代码在后面贴出 @Override @Bean public UserDetailsService userDetailsService(){ return new UserDetailServiceImpl(); } //认证管理 @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth .userDetailsService(userDetailsService()) .passwordEncoder(new BCryptPasswordEncoder()); } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .anyRequest().authenticated() //请求必须经过鉴权认证才能通过 .and().httpBasic()// 在请求头Authorization参数中附带认证编码 .and().cors()//跨域 .and().csrf().disable();//禁用跨站请求伪造 } //权限过滤器,对于一些静态资源和不需要拦截的路由进行配置 @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers( "/error", "/staticunauth/**" ); } }
Oauth2授权服务的相关配置就完成了。
关于服务端,我们也要配置Oauth2,拦截请求必须带有认证信息且认证有效才能访问接口@Configuration @EnableResourceServer @EnableConfigurationProperties(RedisProperties.class) public class ResourceServerConfig extends ResourceServerConfigurerAdapter { @Value("${security.oauth2.resource.id}") public String resourceId; private RedisProperties redisProperties; public ResourceServerConfig(RedisProperties redisProperties ){ this.redisProperties =redisProperties; } @Bean public TokenStore tokenStore() { return new RedisTokenStore(redisConnectionFactory()); } @Bean public RedisConnectionFactory redisConnectionFactory(){ JedisPoolConfig poolConfig = new JedisPoolConfig(); poolConfig.setMaxTotal(100); poolConfig.setMaxIdle(50); poolConfig.setMaxWaitMillis(3000); poolConfig.setTestOnBorrow(true); poolConfig.setTestOnReturn(false); poolConfig.setTestWhileIdle(true); JedisClientConfiguration clientConfig = JedisClientConfiguration.builder() .usePooling().poolConfig(poolConfig).and().readTimeout(Duration.ofMillis(1000)).build(); // 单点redis RedisStandaloneConfiguration redisConfig = new RedisStandaloneConfiguration(); redisConfig.setHostName(redisProperties.getIp()); redisConfig.setPort(redisProperties.getPort()); return new JedisConnectionFactory(redisConfig,clientConfig); } @Override public void configure(ResourceServerSecurityConfigurer resources) throws Exception { resources.resourceId(resourceId) .tokenStore(tokenStore()); } @Override public void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .requestMatchers(EndpointRequest.toAnyEndpoint()).permitAll() .antMatchers( "/v2/api-docs/**", "/swagger-resources/**", "/swagger-ui.html", "/webjars/**" ).permitAll() .anyRequest().authenticated() .and() //统一自定义异常 .exceptionHandling() .and() .csrf().disable(); } }最后
综上,我们完成了授权服务和业务服务的Oauth相关配置。
我们调用xxxx:xxx/oauth/token,传必要的参数,即可获取到返回的access_token信息
grant_type:password client_id:app client_secret:app username:ceshi password:123456
我们在访问业务服务的相关内容时,需要在请求头内添加以下内容,否则提示未授权
Authorization:Bearer edc5890e-8242-4333-859a-ab88cf2062eb
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)