(二)spring security:使用 OAuth2 SSO 实现单点登录

一、前言

    也许,我应该延续上一篇"(一)spring security:能做什么?"接着写,比如:如何实现RBAC权限动态控制、后端"验证码"生成与校验、JWT令牌如何在前后端分离项目中应用、采用"委派密码编码器",兼容老系统密码等,不能原因是,没时间写样例程序(每天好忙o(╯□╰)o),写好的代码又是公司产品源码(担心牵扯事情),以后有机会再补上吧。

    本次处于开发阶段的demo,可以展示了,^_^。

    在阅读这篇文章时,原理和流程,我就不再写了(不一定有别人写的好o(* ̄︶ ̄*)o)。最好先阅读以下参考文献,详细阅读 OAuth 2.0相关文章;这样,你去看别人的博客时,才不会手忙脚乱、不知所措。而且,你会领悟 spring OAuth 2是如何配置、如何实现、授权原理是什么,锻炼自己的思考能力;才能已"架构师"的思想,在企业的应用环境中熟练应用与掌握它。

    下一篇文章,我将已截图和UML图的形式,展示 spring OAuth 2设计的精华所在,堪称艺术品!

    本文所采用的模式是最复杂的 authorization code。演示系统包括 auth_server(授权服务器&资源服务器)、client_demo(客户端)。

    authorization code (授权码)是授权服务器用来获取并作为客户端和资源所有者之间的中介。代替直接向资源所有者请求授权,客户端定向资源所有者到一个授权服务器,授权服务器反过来指导资源所有者将授权码返回给客户端。在将授权码返回给客户端之前,授权服务器对资源所有者进行身份验证并获得授权。因为资源所有者只对授权服务器进行身份验证,所以资源所有者的凭据永远不会与客户机共享。

    先看一张经常见,但又不熟悉的流程图。

它的步骤如下:

  • (A)用户访问客户端,后者将前者导向认证服务器
  • (B)用户选择是否给予客户端授权
  • (C)假设用户给予授权,认证服务器将用户导向客户端事先指定的“重定向 URI”,同时附上一个授权码
  • (D)客户端收到授权码,附上早先的“重定向 URI”向认证服务器申请令牌,这一步是在客户端的后台服务器上完成的,对用户不可见
  • (E)认证服务器核对了授权码和重定向URI,确认无误后向客户端发送令牌和更新令牌

上述步骤中所需要的参数:

  • A步骤中,客户端申请认证的 URI,包含以下参数:
  • response_type:授权类型,必选,此处固定值“code”
  • client_id:客户端的ID,必选
  • client_secret:客户端的密码,可选
  • redirect_uri:重定向URI,可选
  • scope:申请的权限范围,可选
  • state:客户端当前的状态,可以指定任意值,认证服务器会原封不动的返回这个值 (我访问受保护的资源时,框架自动会生成,并附带上)

二、参考文献

(这篇,帮我理解了整个授权流程,实在不错!)

Spring Security Oauth2 单点登录案例实现和执行流程剖析

https://www.cnblogs.com/xifengxiaoma/p/10043173.html

OAuth 2开发人员指南

https://projects.spring.io/spring-security-oauth/docs/oauth2.html

OAuth2 Boot

https://docs.spring.io/spring-security-oauth2-boot/docs/current-SNAPSHOT/reference/html5/

OAuth 2.0
https://www.cnblogs.com/cjsblog/p/9174797.html
Spring Security OAuth 2.0
https://www.cnblogs.com/cjsblog/p/9184173.html
OAuth 2.0 授权码请求
https://www.cnblogs.com/cjsblog/p/9230990.html
Spring Boot OAuth 2.0 客户端
https://www.cnblogs.com/cjsblog/p/9241217.html
OAuth2实现单点登录SSO
https://www.cnblogs.com/cjsblog/p/10548022.html
Spring Security OAuth2 SSO
https://www.cnblogs.com/cjsblog/p/9296361.html

JWT存储token

https://blog.csdn.net/qq_28114159/article/details/106549613?utm_source=app

三、授权服务器配置

1.数据库(沿用上一位博客主的(*^▽^*),其实,在官网也能找到)

--客户端表
CREATE TABLE `oauth_client_details` (
  `client_id` VARCHAR(256) CHARACTER SET utf8 NOT NULL,
  `resource_ids` VARCHAR(256) CHARACTER SET utf8 DEFAULT NULL,
  `client_secret` VARCHAR(256) CHARACTER SET utf8 DEFAULT NULL,
  `scope` VARCHAR(256) CHARACTER SET utf8 DEFAULT NULL,
  `authorized_grant_types` VARCHAR(256) CHARACTER SET utf8 DEFAULT NULL,
  `web_server_redirect_uri` VARCHAR(256) CHARACTER SET utf8 DEFAULT NULL,
  `authorities` VARCHAR(256) CHARACTER SET utf8 DEFAULT NULL,
  `access_token_validity` INT(11) DEFAULT NULL,
  `refresh_token_validity` INT(11) DEFAULT NULL,
  `additional_information` VARCHAR(4096) CHARACTER SET utf8 DEFAULT NULL,
  `autoapprove` VARCHAR(256) CHARACTER SET utf8 DEFAULT NULL,
  PRIMARY KEY (`client_id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8mb4;

-- ----------------------------
-- Records of oauth_client_details
-- ----------------------------
BEGIN;
INSERT INTO `oauth_client_details` VALUES ('OrderManagement', NULL, '$2a$10$8yVwRGY6zB8wv5o0kRgD0ep/HVcvtSZUZsYu/586Egxc1hv3cI9Q6', 'all', 'authorization_code,refresh_token', 'http://localhost:8083/orderSystem/login', NULL, 7200, NULL, NULL, 'true');
INSERT INTO `oauth_client_details` VALUES ('UserManagement', NULL, '$2a$10$ZRmPFVgE6o2aoaK6hv49pOt5BZIKBDLywCaFkuAs6zYmRkpKHgyuO', 'all', 'authorization_code,refresh_token', 'http://localhost:8082/memberSystem/login', NULL, 7200, NULL, NULL, 'true');
COMMIT;

2.maven

这里没有版本号,是因为在顶级 pom.xml里已经定义;请自行添加吧。

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.10.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

        ......

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-springsecurity4</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-web</artifactId>
        </dependency>        
        <dependency>
            <groupId>org.springframework.security.oauth.boot</groupId>
            <artifactId>spring-security-oauth2-autoconfigure</artifactId>
            <version>2.1.10.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-jwt</artifactId>
            <version>1.1.1.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
        </dependency>

3.application-dev.properties

server.port=8080
#系统统一请求前缀,进行uri ant 模式管理
#server.servlet.context-path=/

......

############################## jwt秘钥 ##############################
jwt.signing-key=pp123456
############################## redis缓存 ##############################
spring.session.store-type=redis
spring.redis.host=127.0.0.1
spring.redis.port=6379

......

4.配置"资源服务器"


@SpringBootApplication(scanBasePackages = {"com.xxx.spring.boot"})
public class PrototypeApplication {

	public static void main(String[] args) {
		SpringApplication.run(PrototypeApplication.class, args);
	}
}

@EnableResourceServer

  此类,感觉本次并没有发挥应有的作用,后续研究下,在单点登录中,如何控制资源权限。


/**
 * @desc: 资源服务器配置
 * @author: yanfei
 * @date: 2020/10/28
 */
@Configuration
@EnableResourceServer
public class SsoResourceServerConfig extends ResourceServerConfigurerAdapter {

    @Value("${jwt.signing-key}")
    private String SIGNING_KEY;

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        resources.tokenStore(tokenStore());
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.requestMatcher(new OAuthRequestedMatcher()).authorizeRequests().antMatchers(HttpMethod.OPTIONS).permitAll()
                .anyRequest().authenticated();
    }

    private static class OAuthRequestedMatcher implements RequestMatcher {
        @Override
        public boolean matches(HttpServletRequest request) {
            String auth = request.getHeader("Authorization");
            // Determine if the client request contained an OAuth Authorization
            boolean haveOauth2Token = (auth != null) && auth.startsWith("Bearer");
            boolean haveAccessToken = request.getParameter("access_token") != null;
            return haveOauth2Token || haveAccessToken;
        }
    }

    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey(SIGNING_KEY);
        return converter;
    }

    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(accessTokenConverter());
    }

}

5.@EnableAuthorizationServer

ClientDetailsServiceConfigurer 可用使用 client details service的两种实现中的任意一种:in-memory 或者 JDBC 。

我使用 JDBC,它会在授权过程中,时不时就查询 oauth_client_details表。

/**
 * @desc: 授权服务器配置
 * @author: yanfei
 * @date: 2020/10/26
 */
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private DataSource dataSource;
    @Value("${jwt.signing-key}")
    private String SIGNING_KEY;

    /**
     * 配置 AuthorizationServer安全认证的相关信息,创建ClientCredentialsTokenEndpointFilter核心过滤器
     * @param security
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        //允许客户表单认证
        security.allowFormAuthenticationForClients()
                //设置oauth_client_details中的密码编码器
                .passwordEncoder(new BCryptPasswordEncoder())
                .tokenKeyAccess("isAuthenticated()");
    }

    /**
     * 配置 OAuth2的客户端相关信息(数据库中 oauth_client_details)
     *
     * 客户端授权类型 authorized_grant_types : authorization_code,password,client_credentials,implicit,或refresh_token
     * @param clients
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.jdbc(dataSource);
    }

    /**
     * 配置 AuthorizationServerEndpointsConfigurer众多相关类,包括配置身份认证器,配置认证方式,TokenStore,TokenGranter,OAuth2RequestFactory
     * pathMapping() 方法配置端点URL:
     * /oauth/authorize:授权端点
     * /oauth/token:令牌端点
     * /oauth/confirm_access:用户确认授权提交端点
     * /oauth/error:授权服务错误信息端点
     * /oauth/check_token:用于资源服务访问的令牌解析端点
     * /oauth/token_key:提供公有密匙的端点,如果使用JWT令牌的话
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.accessTokenConverter(jwtAccessTokenConverter())
                .tokenStore(jwtTokenStore());
    }

    @Bean
    public JwtTokenStore jwtTokenStore() {
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
        jwtAccessTokenConverter.setSigningKey(SIGNING_KEY);
        return jwtAccessTokenConverter;
    }
}

6.@EnableWebSecurity

UserDetailsService 的实现类,我就不再贴出来了。很简单的,自己根据自己的用户表,建立 service实现此接口封装就行了。

  这里有些改变,是基于我的系统框架做的配置,所以不再介绍了。后续文章里会找到答案吧。


@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 表单登录接口
     */
    private final String LOGIN_PROCESSING = "/login";

    @Bean
    public UserDetailsService getUserDetailsService() {
        return new BaseAcUserManagerImpl();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //user Details Service验证
        auth.userDetailsService(getUserDetailsService()).passwordEncoder(passwordEncoder());
    }

    /**
     * 采用"委派密码编码器",兼容老系统密码
     * @return
     */
    @Bean
    public static PasswordEncoder passwordEncoder() {
        //默认 bcrypt
        DelegatingPasswordEncoder delegatingPasswordEncoder =
                (DelegatingPasswordEncoder) PasswordEncoderFactories.createDelegatingPasswordEncoder();
        return  delegatingPasswordEncoder;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
                .loginPage(LOGIN_PROCESSING).permitAll()
                .and()
                //端点:/oauth/authorize应该使用Spring Security保护Authorization端点(或其映射的替代物),以便只有经过身份验证的用户才能访问它
                .authorizeRequests()
                .anyRequest()
                .authenticated()
                .and().csrf().disable().cors()
                .and()
                //因为改变"角色投票器"默认的"ROLE_"前缀,重写 AnonymousAuthenticationToken中包含的角色 “ROLE_ANONYMOUS”
                .anonymous().authorities(new String[]{UnifyRoleNoEnum.R_ANONYMOUS.getRoleNo()});
    }

    /**
     * 不走 Spring Security 过滤器链,静态资源放行
     * @param web
     * @throws Exception
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        String staticList = PropKit.use("ant-uri.properties").get("uri.static.list");
        web.ignoring().antMatchers(staticList.split(","));
        super.configure(web);
    }
}

7.登录页和首页

  html也是我沿用的他人的,你们可以从上边的博客中寻找到。

/**
 * @desc:
 * @author: yanfei
 * @date: 2020/10/26
 */
@Controller
public class LoginController {

    @GetMapping("/login")
    public String login() {
        return "login";
    }

    @GetMapping("/")
    public String index() {
        return "index";
    }

}

四、客户端配置

1. application.yml

auth-server: http://localhost:8080

server:
  port: 8083
  servlet:
    context-path: /orderSystem
    session:
      cookie:
        name: CLIENT-ORDER-SESSION
security:
  oauth2:
    client:
      client-id: OrderManagement
      client-secret: order123
      access-token-uri: ${auth-server}/oauth/token
      user-authorization-uri: ${auth-server}/oauth/authorize
    resource:
      jwt:
        key-uri: ${auth-server}/oauth/token_key
#        key-value:
#        key-password:
# user-info-uri,暂时未用到
      user-info-uri: ${auth-server}/user
    sso:
      login-path: /login

2. @EnableOAuth2Sso

【SpringSecurityOAuth2】源码分析@EnableOAuth2Sso在Spring Security OAuth2 SSO单点登录场景下的作用

https://www.cnblogs.com/trust-freedom/p/12002089.html

/**
 * @author yanfei
 * @date 2020/10/28
 */
@EnableOAuth2Sso
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.httpBasic().disable()
                .logout().logoutSuccessUrl("http://localhost:8080/logout")
                .and().antMatcher("/**")
                .authorizeRequests()
                .antMatchers("/", "/login**").permitAll()
                .anyRequest().authenticated()
                .and().csrf().disable();
    }
}

3. 受保护的资源


/**
 * @author yanfei
 * @date 2020/10/28
 */
@Controller
@RequestMapping("/order")
public class OrderController {

    @GetMapping("/list")
    public String list() {
        return "order/list";
    }

}

 

五、数据库 oauth_client_details

OAuth2相关数据表字段的详细说明

https://blog.csdn.net/qq_34997906/article/details/89609297

 

六、演示:"授权码"获得 JWT令牌,访问受保护的资源

七、下期预告

(三)spring security:OAuth2 SSO "授权码"获得 JWT令牌,访问受保护的资源,源码分析流程

 

 

©️2020 CSDN 皮肤主题: 黑客帝国 设计师:上身试试 返回首页