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

生活中,最使人疲惫的往往不是道路的遥远,而是心中的郁闷;最使人痛苦的往往不是生活的不幸,而是希望的破灭;最使人颓废的往往不是前途的坎坷,而是自信的丧失;最使人绝望的往往不是挫折的打击,而是心灵的死亡。所以我们要有自己的梦想,让梦想的星光指引着我们走出落漠,走出惆怅,带着我们走进自己的理想。

导读:本篇文章讲解 Spring Security OAuth2之认证服务中心与资源服务器结合公钥与私钥进行令牌发放与校验、以及JDBC方式下的多客户端授权,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com,来源:原文

前言

使用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>

新建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条初始化数据,这条数据就好比是向支付宝、微信等平台申请创建的一个客户端应用

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

配置授权服务器

声明一个授权服务器继承 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;
    }
}

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

配置Security

继承WebSecurityConfigurerAdapter 使用@EnableWebMvcSecurity 注解开启Spring Security的功能。
@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");//设置执行登录操作的路径
    }
}

自定义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;
    }
}

配置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

搭建资源服务器

配置公钥

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

将公钥拷贝到 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();//其他地址需要认证授权
    }
}

添加访问资源

@RestController
@PreAuthorize("hasAnyAuthority('admin')")
public class AuthController {
    @RequestMapping("/test")
    public String test(){
        return "Hello world!";
    }
}

配置application.properties

server.port=9999
server.servlet.context-path=/resource

四种授权模式

1.授权码模式

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

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

授权码授权流程

1、客户端请求第三方授权 

2、用户(资源拥有者)同意给客户端授权

3、客户端获取到授权码,请求认证服务器申请令牌
 
4、认证服务器向客户端响应令牌 
 
5、客户端请求资源服务器的资源,资源服务校验令牌合法性,完成授权
 
6、资源服务器返回受保护资源

申请授权码

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

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

参数说明:

client_id:客户端id,和授权配置类中设置的客户端id一致, 让资源服务提供者知道是谁在请求

response_type:授权码模式固定为code,要求返回授权码code

scop:客户端范围,和授权配置类中设置的scop一致,要求的授权范围

redirect_uri:跳转uri,当授权码申请成功或拒绝后会跳转到此地址,并在后边带上code参数(授权码)

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

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

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

在这里插入图片描述

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

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

在这里插入图片描述

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

在这里插入图片描述

申请令牌

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

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

在这里插入图片描述

grant_type参数的值是authorization_code,表示采用的授权方式是授权码,

code参数是上一步拿到的授权码,

redirect_uri参数是令牌颁发后的回调网址。

认证失败服务端返回 401 Unauthorized

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

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

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

在这里插入图片描述

access_token:访问令牌,携带此令牌访问资源

token_type:有MAC TokenBearer Token两种类型,两种的校验算法不同,RFC 6750建议Oauth2采用 Bearer Token

refresh_token:刷新令牌,使用此令牌可以延长访问令牌的过期时间。

expires_in:过期时间,单位为秒。

scope:范围,与定义的客户端范围一致。

jti:当前token的唯一标识

校验令牌

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

在这里插入图片描述

exp:过期时间,long类型,距离1970年的秒数(new Date().getTime()可得到当前时间距离1970年的毫秒数)。

user_name: 用户名

client_id:客户端Id,在oauth_client_details中配置

scope:客户端范围,在oauth_client_details表中配置

jti:与令牌对应的唯一标识

在这里插入图片描述

刷新令牌

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

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

http://localhost:8888/auth/oauth/token
grant_type : 固定为 refresh_token,表示要求更新令牌

refresh_token:刷新令牌(注意不是access_token,而是refresh_token),用于更新令牌的令牌

basic认证参数: client_id和client_secret参数用于确认身份

在这里插入图片描述

刷新令牌成功,会重新生成新的访问令牌和刷新令牌,令牌的有效期也比旧令牌长。

刷新令牌通常是在令牌快过期时进行刷新。

资源服务授权

1.资源服务授权过程

1 、客户端请求认证服务申请令牌

2、认证服务生成令牌。认证服务采用非对称加密算法,使用私钥生成令牌。

3、客户端携带令牌访问资源服务。客户端在Http header 中添加: AuthorizationBearer 令牌。

4、资源服务请求认证服务校验令牌的有效性。资源服务接收到令牌,使用公钥校验令牌的合法性。

5、令牌有效,资源服务向客户端响应资源信息

2.资源服务授权测试

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

请求时携带令牌:

在http header中添加 Authorization: Bearer 令牌

在这里插入图片描述

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

2.密码模式

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

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

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

授权过程

1.A网站要求用户提供 B网站的用户名和密码。拿到以后,A 就直接向B请求令牌。

2.B网站验证身份通过后,直接给出令牌。这时不需要跳转,而是把令牌放在JSON数据里面,作为 HTTP 回应,A 因此拿到令牌。

获取令牌

HTTP请求以及包含参数:

http://localhost:8888/auth/oauth/token
grant_type:密码模式授权填写password

username:账号

password:密码

参数使用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

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
grant_type=client_credentials : 表示采用凭证式

client_id和client_secret : 用来让B确认A的身份

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

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

认证中心集成认证服务

认证过程

1、用户登录,请求认证服务 

2、认证服务认证通过,生成jwt令牌,将jwt令牌及相关信息写入Redis,并且将身份令牌写入cookie 

3、用户访问资源页面,携带cookie到网关 

4、网关从cookie获取token,并查询Redis校验token,如果token不存在则拒绝访问,否则放行 

5、用户退出,请求认证服务,清除redis中的token,并且删除cookie中的token 

实现逻辑

token封装

@Data
public class AuthToken implements Serializable{

    //令牌信息 jwt
    String accessToken;
    //刷新token(refresh_token)
    String refreshToken;
    //jwt短令牌
    String jti;
}

登录接口实现

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

登录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);
    }
}

认证测试

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

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/136853.html

(0)
飞熊的头像飞熊bm

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!