前言

微服务中使用Oauth2做授权认证,想要实现以下几点
1、单点登录,所以首先要将认证信息都存储在redis中
2、针对用户名密码方式获取授权,添加更多的细节操作,下面实现的细节只是一些简单的例子

添加引用

<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
  	<groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

先获取一下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<TokenGranter> getDefaultTokenGranters() {
        AuthorizationServerTokenServices tokenServices = tokenServices();
        AuthorizationCodeServices authorizationCodeServices = authorizationCodeServices();
        OAuth2RequestFactory requestFactory = requestFactory();

        List<TokenGranter> 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;
    }

    /**
     * 添加预身份验证
     * @param tokenServices
     * @param userDetailsService
     */
    private void addUserDetailsService(DefaultTokenServices tokenServices, UserDetailsService userDetailsService) {
        if (userDetailsService != null) {
            PreAuthenticatedAuthenticationProvider provider = new PreAuthenticatedAuthenticationProvider();
            provider.setPreAuthenticatedUserDetailsService(new UserDetailsByNameServiceWrapper<PreAuthenticatedAuthenticationToken>(userDetailsService));
            tokenServices.setAuthenticationManager(new ProviderManager(Arrays.<AuthenticationProvider>asList(provider)));
        }
    }

    /**
     * OAuth2RequestFactory的默认实现,它初始化参数映射中的字段,
     * 验证授权类型(grant_type)和范围(scope),并使用客户端的默认值填充范围(scope)(如果缺少这些值)。
     */
    private OAuth2RequestFactory requestFactory() {
        return new DefaultOAuth2RequestFactory(clientDetailsService);
    }

    /**
     * 授权码API
     * @return
     */
    private AuthorizationCodeServices authorizationCodeServices() {
        if (this.authorizationCodeServices == null) {
            this.authorizationCodeServices = new InMemoryAuthorizationCodeServices();
        }
        return this.authorizationCodeServices;
    }
}

继承AbstractTokenGranter ,实现自定义的密码认证

模拟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<String, String> 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<UserAccount> 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<UserAccount> 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);
        }
    }
}

实现UserDetailsService 获取用户信息的具体逻辑

这里主要是根据请求中的username获取数据库中加密后的密码,以便后面的逻辑进行密码匹配
具体逻辑根据实际场景实现

public class UserDetailServiceImpl implements UserDetailsService  {

    @Autowired
    private UserAccountDao userDao;

    @Override
    public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
        //获取本地用户
        QueryWrapper<UserAccount> 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("用户不存在");
        }
    }
}

继承AuthorizationServerConfigurerAdapter 进行授权/认证服务器的配置

@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);
    }

    /**
     * 从数据库读取clientDetails相关配置
     * 有InMemoryClientDetailsService 和 JdbcClientDetailsService 两种方式选择
     */
    @Bean
    public ClientDetailsService clientDetails() {
        return new JdbcClientDetailsService(dataSource);
    }

    /**
     * 注入密码加密实现器
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    /**
     * 设置token有效期
     * @return
     */
    @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;
    }

    /**
     * 认证服务器Endpoints配置 设置为自定义的授权服务
     */
    @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()");
    }

    /**
     * client存储方式,此处使用jdbc存储
     */
    @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());
    }

     /**
     * 方法解释 https://blog.csdn.net/qq_31960623/article/details/120829127
     * anyRequest          |   匹配所有请求路径
     * access              |   SpringEl表达式结果为true时可以访问
     * anonymous           |   匿名可以访问
     * denyAll             |   用户不能访问
     * fullyAuthenticated  |   用户完全认证可以访问(非remember-me下自动登录)
     * hasAnyAuthority     |   如果有参数,参数表示权限,则其中任何一个权限可以访问
     * hasAnyRole          |   如果有参数,参数表示角色,则其中任何一个角色可以访问
     * hasAuthority        |   如果有参数,参数表示权限,则其权限可以访问
     * hasIpAddress        |   如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
     * hasRole             |   如果有参数,参数表示角色,则其角色可以访问
     * permitAll           |   用户可以任意访问
     * rememberMe          |   允许通过remember-me登录的用户访问
     * authenticated       |   用户登录后可访问
     */
    @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",
                "/static/**",
                "/v2/api-docs/**",
                "/swagger-resources/**",
                "/webjars/**",
                "/favicon.ico",
                "/**/unauth/**"
            );
    }

}

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
Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐