• Spring Security OAuth2之认证服务中心与资源服务器结合公钥与私钥进行令牌发放与校验、以及JDBC方式下的多客户端授权


    前言

    使用Spring Security OAuth2搭建认证服务中心、资源服务器、配置JDBC方式下的多客户端。实现:支持多个不同企业、个人应用与支付宝、微信等平台交互,得到他们的资源服务信息的类似效果。

    常见场景:第三方登录、扫描登录等。

    例如:网站微信登录,需要先到微信开发平台申请创建一个应用网站,然后通过申请的应用ID和密匙与微信认证服务器进行通信得到Token,使用Token与微信资源服务器通信,得到想要的资源。

    这里网站应用就是指客户端,也就是不同企业、用户在不同平台下创建的一个客户端应用。

    搭建认证服务中心

    引入依赖

            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
    
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-oauth2</artifactId>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-security</artifactId>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-jdbc</artifactId>
            </dependency>
    	
    	 <dependencyManagement>
            <dependencies>
                <!-- spring cloud 依赖 -->
                <dependency>
                    <groupId>org.springframework.cloud</groupId>
                    <artifactId>spring-cloud-dependencies</artifactId>
                    <version>Hoxton.SR8</version>
                    <type>pom</type>
                    <scope>import</scope>
                </dependency>
            </dependencies>
        </dependencyManagement>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    新建oauth_client_details表

    OAuth2中,客户端信息默认使用内存进行存储。这里通过JdbcClientDetailsService类从数据库的oauth_client_details表内查询客户端列表,在Oauth2源码中会操作该表,以此实现多客户端的添加、删除、更新,这种方式更加灵活方便可控。

    注意: 当使用Jdbc方式来存储认证信息时,即使配置clients参数,也是没有任何作用的,只会使用数据库方式来读取客户端信息

    CREATE TABLE `oauth_client_details` (
      `client_id` varchar(48) NOT NULL COMMENT '客户端ID,主要用于标识对应的应用',
      `resource_ids` varchar(256) DEFAULT NULL,
      `client_secret` varchar(256) DEFAULT NULL COMMENT '客户端秘钥,BCryptPasswordEncoder加密',
      `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,
      `access_token_validity` int(11) DEFAULT NULL COMMENT '令牌有效期',
      `refresh_token_validity` int(11) DEFAULT NULL COMMENT '令牌刷新周期',
      `additional_information` varchar(4096) DEFAULT NULL,
      `autoapprove` varchar(256) DEFAULT NULL,
      PRIMARY KEY (`client_id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    导入1条初始化数据,这条数据就好比是向支付宝、微信等平台申请创建的一个客户端应用

    INSERT INTO `oauth_client_details` VALUES ('zd', null, '$2a$10$PYI5cvzBMRkjqeC6I8KNC.RdzIeDqLGEdeG6sQf1zLqg7e9Kvrsfu', 'web', 'authorization_code,password,refresh_token,client_credentials', 'http://localhost', null, '43200', '43200', null, null);
    
    • 1

    配置授权服务器

    声明一个授权服务器继承 AuthorizationServerConfigurerAdapter,添加@EnableAuthorizationServer注解。

    @Configuration
    //@EnableAuthorizationServer注解告诉Spring这个应用是OAuth2的认证中心
    @EnableAuthorizationServer
    class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
        //数据源,用于从数据库获取数据进行认证操作,测试可以从内存中获取
        @Autowired
        private DataSource dataSource;
        //jwt令牌转换器
        @Autowired
        private JwtAccessTokenConverter jwtAccessTokenConverter;
        //SpringSecurity 用户自定义授权认证类
        @Autowired
        UserDetailsService userDetailsService;
        //授权认证管理器
        @Autowired
        AuthenticationManager authenticationManager;
        //令牌持久化存储接口
        @Autowired
        TokenStore tokenStore;
        @Autowired
        private CustomUserAuthenticationConverter customUserAuthenticationConverter;
    
        /***
         * 客户端信息配置
         * 可以用来定义一个基于内存的或者JDBC的客户端信息服务
         * @param clients
         * @throws Exception
         */
        @Override
        public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
            clients.jdbc(dataSource).clients(clientDetails());
        }
    
        //客户端配置
        @Bean
        public ClientDetailsService clientDetails() {
            return new JdbcClientDetailsService(dataSource);
        }
    
    
        /***
         * 授权服务器端点配置
         * @param endpoints
         * @throws Exception
         */
        @Override
        public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        	// 配置JwtAccessToken转换器
            endpoints.accessTokenConverter(jwtAccessTokenConverter)
                    .authenticationManager(authenticationManager)//认证管理器
                    .tokenStore(tokenStore)                       //令牌存储
                    .userDetailsService(userDetailsService);     //用户信息service
        }
    
        /***
         * 授权服务器的安全配置
         * @param oauthServer
         * @throws Exception
         */
        @Override
        public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
            oauthServer.allowFormAuthenticationForClients()
                    .passwordEncoder(new BCryptPasswordEncoder())
                    .tokenKeyAccess("permitAll()")
                    .checkTokenAccess("isAuthenticated()");
        }
    
    
        //读取密钥的配置
        @Bean("keyProp")
        public KeyProperties keyProperties() {
            return new KeyProperties();
        }
    
        @Resource(name = "keyProp")
        private KeyProperties keyProperties;
    
        @Bean
        @Autowired
        public TokenStore tokenStore(JwtAccessTokenConverter jwtAccessTokenConverter) {
            return new JwtTokenStore(jwtAccessTokenConverter);
        }
    
        /****
         * JWT令牌转换器
         * @param customUserAuthenticationConverter
         * @return
         */
        @Bean
        public JwtAccessTokenConverter jwtAccessTokenConverter(CustomUserAuthenticationConverter customUserAuthenticationConverter) {
            JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
            KeyPair keyPair = new KeyStoreKeyFactory(
                    keyProperties.getKeyStore().getLocation(),                          //证书路径 ybzy.jks
                    keyProperties.getKeyStore().getSecret().toCharArray())              //证书秘钥 adminkeystore
                    .getKeyPair(
                            keyProperties.getKeyStore().getAlias(),                     //证书别名 aliaskey
                            keyProperties.getKeyStore().getPassword().toCharArray());   //证书密码 adminkey
            converter.setKeyPair(keyPair);
            //配置自定义的CustomUserAuthenticationConverter
            DefaultAccessTokenConverter accessTokenConverter = (DefaultAccessTokenConverter) converter.getAccessTokenConverter();
            accessTokenConverter.setUserTokenConverter(customUserAuthenticationConverter);
            return converter;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104

    自定义JwtAccessToken转换器

    @Component
    public class CustomUserAuthenticationConverter extends DefaultUserAuthenticationConverter {
    
        @Autowired
        UserDetailsService userDetailsService;
    
        @Override
        public Map<String, ?> convertUserAuthentication(Authentication authentication) {
            LinkedHashMap response = new LinkedHashMap();
            String name = authentication.getName();
            response.put("username", name);
    
            Object principal = authentication.getPrincipal();
            UserJwt userJwt = null;
            if (principal instanceof UserJwt) {
                userJwt = (UserJwt) principal;
            } else {
                //refresh_token默认不去调用userdetailService获取用户信息,这里手动去调用,得到UserJwt
                UserDetails userDetails = userDetailsService.loadUserByUsername(name);
                userJwt = (UserJwt) userDetails;
            }
            response.put("name", userJwt.getName());
            response.put("id", userJwt.getId());
            if (authentication.getAuthorities() != null && !authentication.getAuthorities().isEmpty()) {
                response.put("authorities", AuthorityUtils.authorityListToSet(authentication.getAuthorities()));
            }
            return response;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    @Data
    public class UserJwt extends User {
        //用户ID
        private String id;
        //用户名字
        private String name; 
    
        public UserJwt(String username, String password, Collection<? extends GrantedAuthority> authorities) {
            super(username, password, authorities);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    配置Security

    继承WebSecurityConfigurerAdapter 使用@EnableWebMvcSecurity 注解开启Spring Security的功能。
    
    • 1
    @Configuration
    @EnableWebSecurity
    @Order(-1)
    class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
        /***
         * 忽略安全拦截的URL
         * @param web
         * @throws Exception
         */
        @Override
        public void configure(WebSecurity web) throws Exception {
            web.ignoring().antMatchers("/oauth/login",
                    "/oauth/logout", "/oauth/toLogin", "/login.html", "/css/**",  "/img/**", "/js/**");
        }
    
        /***
         * 创建授权管理认证对象
         * @return
         * @throws Exception
         */
        @Bean
        @Override
        public AuthenticationManager authenticationManagerBean() throws Exception {
            AuthenticationManager manager = super.authenticationManagerBean();
            return manager;
        }
    
        /***
         * 采用BCryptPasswordEncoder对密码进行编码
         * @return
         */
        @Bean
        public PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    
        /****
         *
         * @param http
         * @throws Exception
         */
        @Override
        public void configure(HttpSecurity http) throws Exception {
            http.csrf().disable()  		// 由于使用的是JWT,这里不需要csrf	
                    .httpBasic()        //启用Http基本身份验证
                    .and()
                    .formLogin()       //启用表单身份验证
                    .and()
                    .authorizeRequests()    //限制基于Request请求访问
                    .anyRequest()
                    .authenticated();       //其他请求都需要经过验证
    
            //开启自定义表单登录
            //http.formLogin().loginPage("/oauth/toLogin")//设置访问登录页面的路径
                   // .loginProcessingUrl("/oauth/login");//设置执行登录操作的路径
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59

    自定义UserDetailsService

    实现UserDetailsService接口,并且重写loadUserByUsername方法,实现登录认证、授权逻辑

    /*****
     * 自定义授权认证类
     */
    @Service
    public class UserDetailsServiceImpl implements UserDetailsService {
    
        @Autowired
        ClientDetailsService clientDetailsService;
    
        @Autowired
        private UserFeign userFeign;
    
        /****
         * 自定义授权认证
         * @param username
         * @return
         * @throws UsernameNotFoundException
         */
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            //取出身份,如果身份为空说明没有认证
            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
            //没有认证统一采用httpbasic认证,httpbasic中存储了client_id和client_secret,开始认证client_id和client_secret
            if (authentication == null) {
                ClientDetails clientDetails = clientDetailsService.loadClientByClientId(username);
                if (clientDetails != null) {
                    //秘钥
                    String clientSecret = clientDetails.getClientSecret();
                    //静态方式
                    //return new User(username,new BCryptPasswordEncoder().encode(clientSecret), AuthorityUtils.commaSeparatedStringToAuthorityList(""));
                    //数据库查找方式
                    return new User(username, clientSecret, AuthorityUtils.commaSeparatedStringToAuthorityList(""));
                }
            }
    
            if (StringUtils.isEmpty(username)) {
                return null;
            }
    
            //TODO 根据用户名查询用户信息,这里构造一个用户密码,
            String pwd = new BCryptPasswordEncoder().encode("userPassword");
            //此角色信息将存在于jwt中 资源服务使用@PreAuthorize("hasAnyAuthority('admin')")进行权限控制
            String permissions = "user,admin";
            UserJwt userDetails = new UserJwt(username, pwd, AuthorityUtils.commaSeparatedStringToAuthorityList(permissions));
            return userDetails;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47

    配置application.yml

    生成私钥和公钥 , 参考 JWT的使用–生成私钥和公钥

    server:
      port: 8888
      servlet:
        context-path: /auth
    spring:
      redis:
        host: 127.0.0.1
        port: 6379
        password:
        jedis:
          pool:
            max-active: 8
            max-idle: 8
            min-idle: 0
      datasource:
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:mysql://127.0.0.1:3306/yb?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true&serverTimezone=UTC
        username: root
        password: 123456
        
    encrypt:
      key-store:
        location: classpath:/ybzy.jks
        secret: adminkeystore
        alias: aliaskey
        password: adminkey
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    搭建资源服务器

    配置公钥

    认证服务生成令牌采用非对称加密算法,认证服务采用私钥加密生成令牌,对外向资源服务提供公钥,资源服务使用公钥 来校验令牌的合法性。

    将公钥拷贝到 publickey.txt文件中,将此文件拷贝到资源服务工程的classpath下

    在这里插入图片描述

    添加资源配置类

    @Configuration
    @EnableResourceServer
    @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)//激活方法上的PreAuthorize注解
    public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    
        /**
         * 公钥
         */
        private static final String PUBLIC_KEY = "publickey.txt";
    
        /**
         * 定义JwtTokenStore,使用jwt令牌
         *
         * @param jwtAccessTokenConverter
         * @return
         */
        @Bean
        public TokenStore tokenStore(JwtAccessTokenConverter jwtAccessTokenConverter) {
            return new JwtTokenStore(jwtAccessTokenConverter);
        }
    
        /**
         * 定义JJwtAccessTokenConverter,使用jwt令牌
         *
         * @return
         */
        @Bean
        public JwtAccessTokenConverter jwtAccessTokenConverter() {
            JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
            converter.setVerifierKey(getPubKey());
            return converter;
        }
    
        /**
         * 获取非对称加密公钥 Key
         *
         * @return 公钥 Key
         */
        private String getPubKey() {
            Resource resource = new ClassPathResource(PUBLIC_KEY);
            try {
                InputStreamReader inputStreamReader = new InputStreamReader(resource.getInputStream());
                BufferedReader br = new BufferedReader(inputStreamReader);
                return br.lines().collect(Collectors.joining("\n"));
            } catch (IOException ioe) {
                return null;
            }
        }
    
        /**
         * Http安全配置,对每个到达系统的http请求链接进行校验
         *
         * @param http
         * @throws Exception
         */
        @Override
        public void configure(HttpSecurity http) throws Exception {
            //所有请求必须认证通过
            http.authorizeRequests()
                    //下边的路径放行
                    .antMatchers("/test1",  "/test2/**").permitAll()
                    .anyRequest().authenticated();//其他地址需要认证授权
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64

    添加访问资源

    @RestController
    @PreAuthorize("hasAnyAuthority('admin')")
    public class AuthController {
        @RequestMapping("/test")
        public String test(){
            return "Hello world!";
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    配置application.properties

    server.port=9999
    server.servlet.context-path=/resource
    
    • 1
    • 2

    四种授权模式

    1.授权码模式

    授权码(authorization code)方式,指的是第三方应用先申请一个授权码,然后再用该码获取令牌。

    最常用的流程,安全性也最高,它适用于那些有后端的 Web 应用。授权码通过前端传送,令牌则是储存在后端,而且所有与资源服务器的通信都在后端完成。

    授权码授权流程

    1、客户端请求第三方授权 
    
    2、用户(资源拥有者)同意给客户端授权
    
    3、客户端获取到授权码,请求认证服务器申请令牌
     
    4、认证服务器向客户端响应令牌 
     
    5、客户端请求资源服务器的资源,资源服务校验令牌合法性,完成授权
     
    6、资源服务器返回受保护资源
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    申请授权码

    1.请求认证服务获取授权码

    A网站提供一个链接,用户点击后就会跳转到B网站,授权用户数据给A网站使用。
    
    • 1
    http://localhost:8888/auth/oauth/authorize?client_id=zd&response_type=code&scop=web&redirect_uri=http://localhost
    
    • 1

    参数说明:

    client_id:客户端id,和授权配置类中设置的客户端id一致, 让资源服务提供者知道是谁在请求
    
    response_type:授权码模式固定为code,要求返回授权码code
    
    scop:客户端范围,和授权配置类中设置的scop一致,要求的授权范围
    
    redirect_uri:跳转uri,当授权码申请成功或拒绝后会跳转到此地址,并在后边带上code参数(授权码)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    2.跳转到登录页面,进行登录

    用户跳转后,B网站会要求用户登录,然后询问是否同意给予 A网站授权。
    
    • 1

    以客户端ID与客户端密匙登录, Spring Security接收到请求会调用UserDetailsService接口的loadUserByUsername方法查询用户正确的密码。

    在这里插入图片描述

    3.进入授权页面,进行授权

    用户表示同意,这时B网站就会跳回redirect_uri参数指定的网址。跳转时,会传回一个授权码
    
    • 1

    在这里插入图片描述

    返回授权码:认证服务携带授权码跳转redirect_uri, 每一个授权码只能使用一次
    
    • 1

    在这里插入图片描述

    申请令牌

    拿到授权码后,申请令牌。

    A网站拿到授权码以后,就可以在后端,向B网站请求令牌。
    
    • 1
    http://localhost:8888/auth/oauth/token
    
    • 1

    在这里插入图片描述

    grant_type参数的值是authorization_code,表示采用的授权方式是授权码,
    
    code参数是上一步拿到的授权码,
    
    redirect_uri参数是令牌颁发后的回调网址。
    
    • 1
    • 2
    • 3
    • 4
    • 5

    认证失败服务端返回 401 Unauthorized

    http Basic认证是http协议定义的一种认证方式,将客户端id和客户端密码按照“客户端ID:客户端密码”的格式拼接,并用base64编码,放在header中请求服务端。

    需要使用 http Basic认证
    在这里插入图片描述

    B网站收到请求以后,就会颁发令牌。具体做法是向redirect_uri指定的网址,发送一段 JSON 数据
    
    • 1

    在这里插入图片描述

    access_token:访问令牌,携带此令牌访问资源
    
    token_type:有MAC TokenBearer Token两种类型,两种的校验算法不同,RFC 6750建议Oauth2采用 Bearer Token
    
    refresh_token:刷新令牌,使用此令牌可以延长访问令牌的过期时间。
    
    expires_in:过期时间,单位为秒。
    
    scope:范围,与定义的客户端范围一致。
    
    jti:当前token的唯一标识
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    校验令牌

    http://localhost:8888/auth/oauth/check_token?token=令牌
    
    • 1

    在这里插入图片描述

    exp:过期时间,long类型,距离1970年的秒数(new Date().getTime()可得到当前时间距离1970年的毫秒数)。
    
    user_name: 用户名
    
    client_id:客户端Id,在oauth_client_details中配置
    
    scope:客户端范围,在oauth_client_details表中配置
    
    jti:与令牌对应的唯一标识
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    在这里插入图片描述

    刷新令牌

    刷新令牌是当令牌快过期时重新生成一个令牌,它于授权码授权和密码授权生成令牌不同,刷新令牌不需要授权码也不需要账号和密码,只需要一个刷新令牌、客户端id和客户端密码。

    B网站颁发令牌的时候,一次性颁发两个令牌,一个用于获取数据,另一个用于获取新的令牌(refresh token 字段)。令牌到期前,用户使用 refresh token 发一个请求,去更新令牌。

    http://localhost:8888/auth/oauth/token
    
    • 1
    grant_type : 固定为 refresh_token,表示要求更新令牌
    
    refresh_token:刷新令牌(注意不是access_token,而是refresh_token),用于更新令牌的令牌
    
    basic认证参数: client_id和client_secret参数用于确认身份
    
    • 1
    • 2
    • 3
    • 4
    • 5

    在这里插入图片描述

    刷新令牌成功,会重新生成新的访问令牌和刷新令牌,令牌的有效期也比旧令牌长。
    
    刷新令牌通常是在令牌快过期时进行刷新。
    
    • 1
    • 2
    • 3

    资源服务授权

    1.资源服务授权过程

    1 、客户端请求认证服务申请令牌
    
    2、认证服务生成令牌。认证服务采用非对称加密算法,使用私钥生成令牌。
    
    3、客户端携带令牌访问资源服务。客户端在Http header 中添加: AuthorizationBearer 令牌。
    
    4、资源服务请求认证服务校验令牌的有效性。资源服务接收到令牌,使用公钥校验令牌的合法性。
    
    5、令牌有效,资源服务向客户端响应资源信息
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    2.资源服务授权测试

    请求时没有携带令牌:
    在这里插入图片描述

    请求时携带令牌:

    在http header中添加 Authorization: Bearer 令牌
    
    • 1

    在这里插入图片描述

    当输入错误的令牌时:
    在这里插入图片描述

    2.密码模式

    密码模式(Resource Owner Password Credentials)与授权码模式的区别是申请令牌不再使用授权码,而是直接通过用户名和密码即可申请令牌

    只适用于其他授权方式都无法采用的情况,而且必须是用户高度信任的应用。

    当令牌没有过期时同一个用户再次申请令牌则不再颁发新令牌。

    授权过程

    1.A网站要求用户提供 B网站的用户名和密码。拿到以后,A 就直接向B请求令牌。
    
    2.B网站验证身份通过后,直接给出令牌。这时不需要跳转,而是把令牌放在JSON数据里面,作为 HTTP 回应,A 因此拿到令牌。
    
    • 1
    • 2
    • 3

    获取令牌

    HTTP请求以及包含参数:

    http://localhost:8888/auth/oauth/token
    
    • 1
    grant_type:密码模式授权填写password
    
    username:账号
    
    password:密码
    
    • 1
    • 2
    • 3
    • 4
    • 5

    参数使用x-www-form-urlencoded方式传输
    在这里插入图片描述
    需要使用 http Basic认证
    在这里插入图片描述
    在这里插入图片描述

    3.隐藏模式

    适用于纯前端应用,必须将令牌储存在前端 允许直接向前端颁发令牌。该方式没有授权码中间步骤,所以称为授权码隐藏式。

    这种方式把令牌直接传给前端,是很不安全的。因此,只能用于一些安全要求不高的场景,并且令牌的有效期必须非常短,通常就是会话期间(session)有效,浏览器关掉,令牌就失效了。

    1.A 网站提供一个链接,要求用户跳转到 B 网站,授权用户数据给 A 网站使用。

    https://b.com/oauth/authorize?response_type=token&client_id=CLIENT_ID&redirect_uri=CALLBACK_URL&scope=read
    
    • 1

    2.用户跳转到 B 网站,登录后同意给予 A 网站授权。这时,B网站就会跳回redirect_uri参数指定的跳转网址,并且把令牌作为URL 参数,传给A网站。

    4.凭证模式

    适用于没有前端的命令行应用,即在命令行下请求令牌。

    1.A 应用在命令行向 B 发出请求。

    https://oauth.b.com/token?grant_type=client_credentials&client_id=CLIENT_ID&client_secret=CLIENT_SECRET
    
    • 1
    grant_type=client_credentials : 表示采用凭证式
    
    client_id和client_secret : 用来让B确认A的身份
    
    • 1
    • 2
    • 3

    2.B 网站验证通过以后,直接返回令牌。

    这种方式给出的令牌,是针对第三方应用的,而不是针对用户的,即有可能多个用户共享同一个令牌。

    认证中心集成认证服务

    认证过程

    1、用户登录,请求认证服务 
    
    2、认证服务认证通过,生成jwt令牌,将jwt令牌及相关信息写入Redis,并且将身份令牌写入cookie 
    
    3、用户访问资源页面,携带cookie到网关 
    
    4、网关从cookie获取token,并查询Redis校验token,如果token不存在则拒绝访问,否则放行 
    
    5、用户退出,请求认证服务,清除redis中的token,并且删除cookie中的token 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    实现逻辑

    token封装

    @Data
    public class AuthToken implements Serializable{
    
        //令牌信息 jwt
        String accessToken;
        //刷新token(refresh_token)
        String refreshToken;
        //jwt短令牌
        String jti;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    登录接口实现

    public interface AuthService {
        /**
         * 登录时申请令牌
         * @param username 账号
         * @param password 密码
         * @param clientId 客户端ID
         * @param clientSecret 客户端密码
         * @return
         */
        AuthToken login(String username, String password, String clientId, String clientSecret);
    }
    
    @Service
    public class AuthServiceImpl implements AuthService {
    
        @Autowired
        private StringRedisTemplate stringRedisTemplate;
    
        @Override
        public AuthToken login(String username, String password, String clientId, String clientSecret) {
            //申请令牌地址
            String url = "http://localhost:8888/auth/oauth/token";
    
            //封装请求参数
            MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
            body.add("grant_type", "password");
            body.add("username", username);
            body.add("password", password);
    
            //封装请求头信息
            MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
            headers.add("Authorization", this.getHttpBasic(clientId, clientSecret));
    
            HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(body, headers);
    
            RestTemplate restTemplate = new RestTemplate();
            restTemplate.setErrorHandler(new DefaultResponseErrorHandler() {
                @Override
                public void handleError(ClientHttpResponse response) throws IOException {
                    if (response.getRawStatusCode() != 400 && response.getRawStatusCode() != 401) {
                        super.handleError(response);
                    }
                }
            });
            ResponseEntity<Map> responseEntity = restTemplate.exchange(url, HttpMethod.POST, requestEntity, Map.class);
    
            //处理相应结果
            Map map = responseEntity.getBody();
            if (map == null || map.get("access_token") == null || map.get("refresh_token") == null || map.get("jti") == null) {
                //申请令牌失败
                throw new RuntimeException("申请令牌失败");
            }
    
            //封装结果数据
            AuthToken authToken = new AuthToken();
            authToken.setAccessToken((String) map.get("access_token"));
            authToken.setRefreshToken((String) map.get("refresh_token"));
            authToken.setJti((String) map.get("jti"));
    
            //将jwt存入Redis
            stringRedisTemplate.boundValueOps(authToken.getJti()).set(authToken.getAccessToken(), 3600, TimeUnit.SECONDS);
            return authToken;
        }
    
        /**
         * 客户端ID:客户端密码 进行base64编码
         *
         * @param clientId     客户端ID
         * @param clientSecret 客户端密码
         * @return
         */
        private String getHttpBasic(String clientId, String clientSecret) {
            String value = clientId + ":" + clientSecret;
            byte[] encode = Base64Utils.encode(value.getBytes());
            return "Basic " + new String(encode);
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78

    登录API

    @RestController
    @RequestMapping("/oauth")
    public class AuthController {
    
        @Autowired
        private AuthService authService;
    
        @RequestMapping("/login")
        @ResponseBody
        public Result login(String username, String password, HttpServletResponse response) {
            if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) {
                throw new RuntimeException("用户名或密码不能为空");
            }
    
            //申请令牌
            AuthToken authToken = authService.login(username, password, "zd", "zd");
    
            //将jti的值存入cookie中
            this.saveJtiToCookie(authToken.getJti(), response);
    
            return new Result(true, 200, "登录成功", authToken.getJti());
        }
    
        //将令牌的短标识jti存入到cookie中
        private void saveJtiToCookie(String jti, HttpServletResponse response) {
            Cookie cookie = new Cookie("uid", jti);
            cookie.setDomain("localhost");
            cookie.setPath("/");
            cookie.setMaxAge(-1);
            cookie.setHttpOnly(false);
            response.addCookie(cookie);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    认证测试

    在这里插入图片描述
    在这里插入图片描述

  • 相关阅读:
    Mysql主从切换流程
    HTTP请求偶尔失败(21秒后超时) - 问题排查
    01背包学习笔记-二维dp数组问题
    Springboot 整合 企业微信机器人助手推送消息
    VR失重太空舱游乐设备|航空航天VR体验|VR航天航空体验馆
    如何学好机器学习?机器学习一定需要下面这几方面知识!
    【华为OD机考B卷 | 100分】整数编码(JAVA题解——也许是全网最详)
    性能测试 —— 吞吐量和并发量的关系? 有什么区别?
    JVM常用概念之扁平化堆容器
    redis缓存设计与优化
  • 原文地址:https://blog.csdn.net/qq_38628046/article/details/113407374