核心 POM

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-oauth2</artifactId>
  	<version>2.2.6.RELEASE</version>
</dependency>

配置 WebSecurityConfig 开启 Spring 方法级的安全保护

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;

/**
 * 开启Spring方法级的安全保护
 */
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true) 
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 授权码模式在浏览器地址栏发起请求来获取 code
     * .anyRequest().authenticated() 必须对该请求进行认证拦截,发现用户没有登陆的时候会弹出登陆框, 从而让用户输入用户名和密码进行登陆, 若是对该请求进行放行, 则登陆页无法弹出, 并抛出 InsufficientAuthenticationException
     * .httpBasic() 因为用户未登陆访问了受保护的资源, 所以还要开启 httpBasic 进行简单认证, 否则会抛出 AccessDeniedException 异常,
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()//关闭跨域保护
                .authorizeRequests()
                .antMatchers("/captcha/**").permitAll()
                .anyRequest().authenticated()
                .and()
                .httpBasic()
                .and()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)//永远不会创建HttpSession, 默认配置
                .and()
                .headers().cacheControl().disable()//禁用缓存
        ;
    }

    /**
     * 注入一个认证管理器, 自身不实现身份验证, 而是逐一向认证提供者进行认证, 直到某一个认证提供者能够成功验证当前用户的身份
     * 
     * AuthenticationManager(认证管理器接口) 的默认实现类 ProviderManager, 管理多个 AuthenticationProvider(认证提供者)
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

}

配置 TokenConfig 来定义 Token 的生成方式

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;

/**
 * 配置token
 */
@Configuration
public class TokenConfig {

    @Bean
    public TokenStore jwtTokenStore() {
        //令牌存储方案采用JWT
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    /*
     * AccessToken转换器: 定义 token 的生成方式
     * JwtAccessTokenConverter: 表示采用 JWT 来生成
     */
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey(OauthConstant.OAUTH_SIGNING_KEY); //对称秘钥,资源服务器使用该秘钥来验证
        return converter;
    }

}

配置 UserDetailsService 和 JdbcClientDetailsService 获取用户和客户端信息

自定义客户端表结构
-- oauth2.0 默认有一套客户端表结构可进行替换,以下为自定义,其他用户的表结构就自行定义吧
CREATE TABLE `sys_client` (
  `id` int NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `client_id` varchar(32) NOT NULL COMMENT '客户端ID',
  `resource_ids` varchar(256) DEFAULT NULL COMMENT '客户端密钥',
  `client_secret` varchar(256) DEFAULT NULL COMMENT '资源ID列表',
  `scope` varchar(256) DEFAULT NULL COMMENT '作用域',
  `authorized_grant_types` varchar(256) DEFAULT NULL COMMENT '授权方式',
  `web_server_redirect_uri` varchar(256) DEFAULT NULL COMMENT '回调地址',
  `authorities` varchar(256) DEFAULT NULL COMMENT '权限列表',
  `access_token_validity` int DEFAULT NULL COMMENT '令牌有效时间',
  `refresh_token_validity` int DEFAULT NULL COMMENT '刷新令牌有效时间',
  `additional_information` varchar(4096) DEFAULT NULL COMMENT '扩展信息',
  `autoapprove` varchar(256) DEFAULT NULL COMMENT '是否自动放行',
  `deleted` char(1) DEFAULT '0' COMMENT '删除标记,1:已删除,0:正常',
  `platform_id` int NOT NULL DEFAULT '0' COMMENT '所属平台',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='客户端信息表';

-- 插入两条测试客户端
INSERT INTO `platform`.`sys_client`(`id`, `client_id`, `resource_ids`, `client_secret`, `scope`, `authorized_grant_types`, `web_server_redirect_uri`, `authorities`, `access_token_validity`, `refresh_token_validity`, `additional_information`, `autoapprove`, `deleted`, `platform_id`) VALUES (1, 'c1', '', '$2a$10$1Bg3qCxNVXobR2SJG9t0zOV45glOCH1MpvvPJDdyXCycWu/rZ1DOa', 'all', 'refresh_token,authorization_code,client_credentials,implicit', 'http://www.baidu.com', NULL, 72000, 259200, NULL, 'false', '0', 0);
INSERT INTO `platform`.`sys_client`(`id`, `client_id`, `resource_ids`, `client_secret`, `scope`, `authorized_grant_types`, `web_server_redirect_uri`, `authorities`, `access_token_validity`, `refresh_token_validity`, `additional_information`, `autoapprove`, `deleted`, `platform_id`) VALUES (2, 'c2', 'mo-wen-res', '$2a$10$1Bg3qCxNVXobR2SJG9t0zOV45glOCH1MpvvPJDdyXCycWu/rZ1DOa', 'all', 'password,refresh_token,captcha,authorization_code', 'http://www.baidu.com', NULL, 72000, 259200, NULL, 'false', '0', 0);
-- 查询客户端信息
String OAUTH_SQL_CLIENT = "SELECT client_id, client_secret, resource_ids, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, autoapprove\n" +
            "FROM sys_client WHERE client_id = ? AND deleted = 0";

-- 查询用户信息
String OAUTH_SQL_LOGIN_USER = "select user_id as id, `name`, phone, password from sys_user where phone = ?";

-- 查询用户权限
String OAUTH_SQL_USER_PERMISSION = "SELECT `name` FROM sys_permission WHERE id IN(\n" +
            "      SELECT permission_id FROM sys_role_permission WHERE role_id IN(\n" +
            "          SELECT role_id FROM sys_user_role WHERE user_id = ?\n" +
            "      )\n" +
            ")";
定义 UserDetailsService 的实现类用于查询用户信息
@Slf4j
@Service
@AllArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {

    // 查询用户信息的service, 自行定义
    private final UserService userService;

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        LoginUser loginUser = userService.getUserByPhone(s);
        List<String> authorities = userService.getPermissionsByUserId(loginUser.getId());
        log.info("当前登陆用户: [{}] 权限: [{}]", loginUser, authorities);
        return User.withUsername(toJSONString(loginUser))
                .password(loginUser.getPassword())
                .authorities(authorities.toArray(new String[0]))
                .build();
    }

}
重写 JdbcClientDetailsService 用于查询客户端信息
@Slf4j
@Service
public class JdbcClientDetailsServiceImpl extends JdbcClientDetailsService {

    public JdbcClientDetailsServiceImpl(DataSource dataSource) {
        super(dataSource);
    }

    @Resource
    private PasswordEncoder bCryptPasswordEncoder;

    @Override
    public ClientDetails loadClientByClientId(String clientId) throws InvalidClientException {
        super.setSelectClientDetailsSql(OauthConstant.OAUTH_SQL_CLIENT);
        super.setPasswordEncoder(bCryptPasswordEncoder);
        ClientDetails clientDetails = super.loadClientByClientId(clientId);
        log.info("加载客户端信息: [{}], [{}]", clientId, clientDetails);
        return clientDetails;
    }

    //用于密码的加密方式
    @Bean
    public PasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

}

继承 AuthorizationServerConfigurerAdapter 来实现认证服务器的核心配置❤️❤️❤️❤️❤️❤️❤️❤️❤️❤️❤️❤️

@Configuration
@EnableAuthorizationServer
@AllArgsConstructor
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    private final JdbcClientDetailsServiceImpl clientDetailsServiceImpl;
    private final UserDetailsServiceImpl userDetailsServiceImpl;
    private final TokenStore jwtTokenStore;
    private final JwtAccessTokenConverter jwtAccessTokenConverter;
    private final AuthenticationManager authenticationManager;
    private final PasswordEncoder bCryptPasswordEncoder;
    private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint;
    private final CustomWebResponseExceptionTranslator customWebResponseExceptionTranslator;

    /**
     * 用来配置令牌端点的安全约束, 密码校验方式等
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security
            	.allowFormAuthenticationForClients()
                .passwordEncoder(bCryptPasswordEncoder)
                .tokenKeyAccess("permitAll()")                    //oauth/token_key是公开
                .checkTokenAccess("permitAll()")                  //oauth/check_token公开
        ;
    }

    /**
     * 用来配置客户端详情服务(ClientDetailsService), 客户端详情信息在这里进行初始化
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.withClientDetails(clientDetailsServiceImpl);
    }

    /**
     * 用来配置令牌(token)的访问端点和令牌服务
     * 配置令牌的访问端点:即申请令牌的URL .pathMapping(defaultPath, customPath)
     * 令牌服务:令牌的生成和发放规则(TokenConfig)
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
                .userDetailsService(userDetailsServiceImpl)
                .authenticationManager(authenticationManager)//认证管理器,Password模式配置所需
                .authorizationCodeServices(authorizationCodeServices())//授权码模式code存储方式定义
                .tokenStore(jwtTokenStore)//采用jwt方式管理token
                .accessTokenConverter(jwtAccessTokenConverter)//jwt增强,定义自己的SigningKey
                .allowedTokenEndpointRequestMethods(HttpMethod.POST, HttpMethod.GET)
                .reuseRefreshTokens(false)//表示重复使用刷新令牌。也就是说会一直重复使用第一次请求到的 refresh_token, 所以要禁止掉
        ;
    }

    @Bean
    public AuthorizationCodeServices authorizationCodeServices() { 
        //设置授权码模式的授权码如何存取,暂采用内存方式
        return new InMemoryAuthorizationCodeServices();
    }

}

测试端口

  • 获取 code:Get /oauth/authorize?client_id=c1&response_type=code&scope=all&redirect_uri=http://www.baidu.com
  • 简化模式获取 token:Get /oauth/authorize?response_type=token&client_id=c1&redirect_uri=http://www.baidu.com
  • 密码模式获取 token:Post /oauth/token
    • grant_type:password
    • username:用户名
    • password:密码
    • Authorization:Basic YzI6NDU2 — 请求头添加客户端信息
  • 授权码模式获取 token:Post /oauth/token
    • grant_type:authorization_code
    • code:获取到的 code
    • redirect_uri:重定向url
    • Authorization:Basic YzI6NDU2 — 请求头添加客户端信息
  • 刷新 token:Post /oauth/token
    • grant_type:refresh_token
    • refresh_token:拿到获取token时得到的refresh_token
    • Authorization:Basic YzI6NDU2 — 请求头添加客户端信息

Oauth2.0 系列文章

以下是同步到语雀的、可读性好一点,CSDN 继续看的点专栏就好。
Oauth2.0 核心篇
Oauth2.0 安全性(以微信授权登陆为例)
Oauth2.0 认证服务器搭建
Oauth2.0 添加验证码登陆方式
Oauth2.0 资源服务器搭建
Oauth2.0 自定义响应值以及异常处理
Oauth2.0 补充

Logo

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

更多推荐