User-Profile-Image
hankin
  • 5
  • Java
  • Kotlin
  • Spring
  • Web
  • SQL
  • MegaData
  • More
  • Experience
  • Enamiĝu al vi
  • 分类
    • Zuul
    • Zookeeper
    • XML
    • WebSocket
    • Web Notes
    • Web
    • Vue
    • Thymeleaf
    • SQL Server
    • SQL Notes
    • SQL
    • SpringSecurity
    • SpringMVC
    • SpringJPA
    • SpringCloud
    • SpringBoot
    • Spring Notes
    • Spring
    • Servlet
    • Ribbon
    • Redis
    • RabbitMQ
    • Python
    • PostgreSQL
    • OAuth2
    • NOSQL
    • Netty
    • MySQL
    • MyBatis
    • More
    • MinIO
    • MegaData
    • Maven
    • LoadBalancer
    • Kotlin Notes
    • Kotlin
    • Kafka
    • jQuery
    • JavaScript
    • Java Notes
    • Java
    • Hystrix
    • Git
    • Gateway
    • Freemarker
    • Feign
    • Eureka
    • ElasticSearch
    • Docker
    • Consul
    • Ajax
    • ActiveMQ
  • 页面
    • 归档
    • 摘要
    • 杂图
    • 问题随笔
  • 友链
    • Spring Cloud Alibaba
    • Spring Cloud Alibaba - 指南
    • Spring Cloud
    • Nacos
    • Docker
    • ElasticSearch
    • Kotlin中文版
    • Kotlin易百
    • KotlinWeb3
    • KotlinNhooo
    • 前端开源搜索
    • Ktorm ORM
    • Ktorm-KSP
    • Ebean ORM
    • Maven
    • 江南一点雨
    • 江南国际站
    • 设计模式
    • 熊猫大佬
    • java学习
    • kotlin函数查询
    • Istio 服务网格
    • istio
    • Ktor 异步 Web 框架
    • PostGis
    • kuangstudy
    • 源码地图
    • it教程吧
    • Arthas-JVM调优
    • Electron
    • bugstack虫洞栈
    • github大佬宝典
    • Sa-Token
    • 前端技术胖
    • bennyhuo-Kt大佬
    • Rickiyang博客
    • 李大辉大佬博客
    • KOIN
    • SQLDelight
    • Exposed-Kt-ORM
    • Javalin—Web 框架
    • http4k—HTTP包
    • 爱威尔大佬
    • 小土豆
    • 小胖哥安全框架
    • 负雪明烛刷题
    • Kotlin-FP-Arrow
    • Lua参考手册
    • 美团文章
    • Java 全栈知识体系
    • 尼恩架构师学习
    • 现代 JavaScript 教程
    • GO相关文档
    • Go学习导航
    • GoCN社区
    • GO极客兔兔-案例
    • 讯飞星火GPT
    • Hollis博客
    • PostgreSQL德哥
    • 优质博客推荐
    • 半兽人大佬
    • 系列教程
    • PostgreSQL文章
    • 云原生资料库
    • 并发博客大佬
Help?

Please contact us on our email for need any support

Support
    首页   ›   Spring   ›   SpringSecurity   ›   OAuth2   ›   正文
OAuth2

SpringSecurity—OAuth 2(二)授权码模式

2020-09-12 22:44:18
1207  0 0
参考目录 隐藏
1) 完整例子:
2) 1、授权服务器搭建
3) 2、资源服务器搭建
4) 3、 第三方应用搭建
5) 5、测试

阅读完需:约 16 分钟

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

这种方式是最常用的流程,安全性也最高,它适用于那些有后端的 Web 应用。授权码通过前端传送,令牌则是储存在后端,而且所有与资源服务器的通信都在后端完成。这样的前后端分离,可以避免令牌泄漏。


授权码模式(authorization code)是功能最完整、流程最严密的授权模式。它的特点就是通过客户端的后台服务器,与”服务提供商”的认证服务器进行互动。

它的步骤如下: (下面的代码例子和这个流程是一样的)

(A)用户访问客户端,后者将前者导向认证服务器。

(B)用户选择是否给予客户端授权。

(C)假设用户给予授权,认证服务器将用户导向客户端事先指定的”重定向URI”(redirection URI),同时附上一个授权码。

(D)客户端收到授权码,附上早先的”重定向URI”,向认证服务器申请令牌。这一步是在客户端的后台的服务器上完成的,对用户不可见。

(E)认证服务器核对了授权码和重定向URI,确认无误后,向客户端发送访问令牌(access token)和更新令牌(refresh token)。


完整例子:

auth-server:

user-server:

client-app:

我们常见的 OAuth2 授权码模式登录中,涉及到的各个角色,我都会自己提供,自己测试,这样可以最大限度的让小伙伴们了解到 OAuth2 的工作原理。


1、授权服务器搭建

首先我们搭建一个名为 auth-server 的授权服务

项目创建完成后,首先提供一个 Spring Security 的基本配置:

SecurityConfig:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    //密码编码器
    @Bean
    PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //在内存身份验证中
        auth.inMemoryAuthentication()
                .withUser("sang").password(passwordEncoder().encode("123"))
                .roles("admin")
                .and()
                .withUser("xjh").password(passwordEncoder().encode("123"))
                .roles("user");
    }

    //配置
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable().formLogin();
    }

    /*
    * 认证管理器
    * */
    @Override
    @Bean
    protected AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }
}

在这段代码中,为了代码简洁,我就不把 Spring Security 用户存到数据库中去了,直接存在内存中。这里我创建了一个名为 sang 的用户,密码是 123,角色是 admin。同时我还配置了一个表单登录。这段配置的目的,实际上就是配置用户。例如你想用微信登录第三方网站,在这个过程中,你得先登录微信,登录微信就要你的用户名/密码信息,那么我们在这里配置的,其实就是用户的用户名/密码/角色信息。

基本的用户信息配置完成后, 接下来我们来配置授权服务器:

AccessTokenConfig

@Configuration
public class AccessTokenConfig  {
    
    @Bean
    TokenStore tokenStore(){
        //在内存中存储令牌
        return new InMemoryTokenStore();
    }
}

AuthorizationServerConfig:

/**
 * @author Lenovo
 * 授权服务器配置器适配器
 */
@Configuration
@EnableAuthorizationServer  //启用授权服务器
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
    @Autowired
    TokenStore tokenStore;

    @Autowired
    PasswordEncoder passwordEncoder;

    /*
    * 认证管理器 为密码模式准备的
    */
    @Autowired
    AuthenticationManager authenticationManager;

    /**
     *客户详情服务
     */
    @Autowired
    ClientDetailsService clientDetailsService;

    @Bean //授权服务器令牌服务
    AuthorizationServerTokenServices tokenServices(){
        //默认令牌服务
        DefaultTokenServices services=new DefaultTokenServices();
        //设置客户详细信息服务
        services.setClientDetailsService(clientDetailsService);
        //设置支持刷新令牌
        services.setSupportRefreshToken(true);
        //设置令牌存储
        services.setTokenStore(tokenStore);
        //设置访问令牌有效性秒数
        services.setAccessTokenValiditySeconds(60*60*2);
        //设置刷新令牌有效秒数
        services.setRefreshTokenValiditySeconds(60*60*24*7);
        return services;
    }

    //授权服务器安全配置器
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.checkTokenAccess("permitAll()")//检查令牌访问
                .allowFormAuthenticationForClients();//允许客户端进行表单身份验证
    }

    //客户端详细信息服务配置
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()//在内存中
                //用户
                .withClient("xjh")
                //密码
                .secret(passwordEncoder.encode("123"))
                //资源编号
                .resourceIds("res1")
                //授权的赠款类型 authorization_code 授权码模式,refresh_token 刷新Token , implicit 简化模式 , password 密码模式 , 需要哪个模式就配置哪个模式
                .authorizedGrantTypes("authorization_code","refresh_token","implicit","password","client_credentials")
                //范围
                .scopes("all")
                //自动批准
                .autoApprove(true)
                //重定向Uris
                .redirectUris("http://localhost:8082/index.html","http://localhost:8082/password.html");
    }

    //授权服务器端点配置器
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints //认证管理器 为密码模式准备的
                .authenticationManager(authenticationManager)
                //授权码服务
                .authorizationCodeServices(authorizationCodeServices())
                .tokenServices(tokenServices());
    }

    //授权码服务
    AuthorizationCodeServices authorizationCodeServices(){
        return new InMemoryAuthorizationCodeServices();
    }
}

这段代码有点长,我来给大家挨个解释:

  • 1. 首先我们提供了一个 TokenStore 的实例,这个是指你生成的 Token 要往哪里存储,我们可以存在 Redis 中,也可以存在内存中,也可以结合 JWT 等等,这里,我们就先把它存在内存中,所以提供一个 InMemoryTokenStore 的实例即可。
  • 2. 接下来我们创建 AuthorizationServer 类继承自 AuthorizationServerConfigurerAdapter,来对授权服务器做进一步的详细配置,AuthorizationServer 类记得加上@EnableAuthorizationServer 注解,表示开启授权服务器的自动化配置。
  • 3. 在 AuthorizationServer 类中,我们其实主要重写三个 configure 方法。
  • 4. AuthorizationServerSecurityConfigurer 用来配置令牌端点的安全约束,也就是这个端点谁能访问,谁不能访问。checkTokenAccess 是指一个 Token 校验的端点,这个端点我们设置为可以直接访问(在后面,当资源服务器收到 Token 之后,需要去校验 Token 的合法性,就会访问这个端点)。
  • 5. ClientDetailsServiceConfigurer 用来配置客户端的详细信息,授权服务器要做两方面的检验,一方面是校验客户端,另一方面则是校验用户,校验用户,我们前面已经配置了,这里就是配置校验客户端。客户端的信息我们可以存在数据库中,这其实也是比较容易的,和用户信息存到数据库中类似,但是这里为了简化代码,我还是将客户端信息存在内存中,这里我们分别配置了客户端的 id,secret、资源 id、授权类型、授权范围以及重定向 uri。授权类型一共讲了四种,四种之中不包含 refresh_token 这种类型,但是在实际操作中,refresh_token 也被算作一种。
  • 6. AuthorizationServerEndpointsConfigurer 这里用来配置令牌的访问端点和令牌服务。authorizationCodeServices用来配置授权码的存储,这里我们是存在在内存中,tokenServices 用来配置令牌的存储,即 access_token 的存储位置,这里我们也先存储在内存中。授权码和令牌有什么区别?授权码是用来获取令牌的,使用一次就失效,令牌则是用来获取资 源
  • 7. tokenServices 这个 Bean 主要用来配置 Token 的一些基本信息,例如 Token 是否支持刷新、Token 的存储位置、Token 的有效期以及刷新 Token 的有效期等等。Token 有效期这个好理解,刷新 Token 的有效期我说一下,当 Token 快要过期的时候,我们需要获取一个新的 Token,在获取新的 Token 时候,需要有一个凭证信息,这个凭证信息不是旧的 Token,而是另外一个refresh_token,这个 refresh_token 也是有有效期的。

2、资源服务器搭建

接下来我们搭建一个资源服务器。大家网上看到的例子,资源服务器大多都是和授权服务器放在一起的,如果项目比较小的话,这样做是没问题的,但是如果是一个大项目,这种做法就不合适了。资源服务器就是用来存放用户的资源,例如你在微信上的图像、openid 等信息,用户从授权服务器上拿到 access_token 之后,接下来就可以通过 access_token 来资源服务器请求数据。我们创建一个新的 Spring Boot 项目,叫做 user-server ,作为我们的资源服务器,创建时,添加如下依赖:

项目创建成功之后,添加如下配置:

ResourceServerConfig :

/**
 * @author Lenovo
 * 资源服务器配置适配器
 */
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    @Bean
    RemoteTokenServices tokenServices(){
        //远程令牌服务
        RemoteTokenServices services=new RemoteTokenServices();
        //设置检查令牌端点网址
        services.setCheckTokenEndpointUrl("http://localhost:8080/oauth/check_token");
        //设定客户编号
        services.setClientId("xjh");
        //设置客户机密
        services.setClientSecret("123");
        return services;
    }

    //资源服务器安全配置器
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        //资源编号
        resources.resourceId("res1").tokenServices(tokenServices());
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()//授权请求
                .antMatchers("/admin/**").hasRole("admin")
                .anyRequest().authenticated()
                .and().cors();//所有要求都过
    }
}

这段配置代码很简单,我简单的说一下:

  • 1. tokenServices 我们配置了一个 RemoteTokenServices 的实例,这是因为资源服务器和授权服务器是分开的,资源服务器和授权服务器是放在一起的,就不需要配置 RemoteTokenServices 了。
  • 2. RemoteTokenServices 中我们配置了 access_token 的校验地址、client_id、client_secret 这三个信息,当用户来资源服务器请求资源时,会携带上一个 access_token,通过这里的配置,就能够校验出 token 是否正确等。
  • 3. 最后配置一下资源的拦截规则,这就是 Spring Security 中的基本写法,我就不再赘述。接下来我们再来配置两个测试接口:

HelloController:

@RestController
@CrossOrigin(value = "*")
public class HelloController {
    @GetMapping("/hello")
    public String hello(){
        return "Hello";
    }

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

如此之后,我们的资源服务器就算配置成功了。


3、 第三方应用搭建

第三方应用就是一个普通的 Spring Boot 工程,创建时加入 Thymeleaf 依赖和 Web 依赖:

在 resources/templates 目录下,创建 index.html :

index.html :

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<a href="http://localhost:8080/oauth/authorize?client_id=xjh&response_type=code&scope=all
&redirect_uri=http://localhost:8082/index.html">
    第三方登录
</a>
<h1 th:text="${msg}"></h1>
</body>
</html>

这是一段 Thymeleaf 模版,点击超链接就可以实现第三方登录,超链接的参数如下:

  • client_id 客户端 ID,根据我们在授权服务器中的实际配置填写。
  • response_type 表示响应类型,这里是 code 表示响应一个授权码。
  • redirect_uri 表示授权成功后的重定向地址,这里表示回到第三方应用的首页。
  • scope 表示授权范围。

h1 标签中的数据是来自资源服务器的,当授权服务器通过后,我们拿着 access_token 去资源服务器加载数据,加载到的数据就在 h1 标签中显示出来。

接下来我们来定义一个 HelloController:

HelloController :

@Controller
public class HelloController {
    @Autowired
    RestTemplate restTemplate;

    @GetMapping("/index.html")
    public String index(String code , Model model){
        if(code!=null){
            System.out.println(code);
            //一个key对应多个value,通常我们会将多个value放到一个集合中
            MultiValueMap<String,String> map=new LinkedMultiValueMap();
            map.add("code",code);
            map.add("client_id","xjh");
            map.add("client_secret","123");
            map.add("redirect_uri","http://localhost:8082/index.html");
            map.add("grant_type","authorization_code");
            Map<String,String> resp = restTemplate.postForObject("http://localhost:8080/oauth/token", map, Map.class);
            System.out.println("resp="+resp);

            HttpHeaders headers=new HttpHeaders();
            headers.add("Authorization","Bearer"+resp.get("access_token"));
            HttpEntity<?> httpEntity=new HttpEntity<>(headers);
            ResponseEntity<String> responseEntity = restTemplate.exchange("http://localhost:8081/hello", HttpMethod.GET, httpEntity, String.class);
            model.addAttribute("msg",responseEntity.getBody());
        }
        return "index";
    }
}

在这个 HelloController 中,我们定义出 /index.html 的地址。

  • code:表示授权码,必选项。该码的有效期应该很短,通常设为10分钟,客户端只能使用该码一次,否则会被授权服务器拒绝。该码与客户端ID和重定向URI,是一一对应关系。
  • state:如果客户端的请求中包含这个参数,认证服务器的回应也必须一模一样包含这个参数。
  • grant_type:表示使用的授权模式,必选项,此处的值固定为”authorization_code“。
  • code:表示上一步获得的授权码,必选项。
  • redirect_uri:表示重定向URI,必选项,且必须与A步骤中的该参数值保持一致。(必须和一开始发送给认证服务器的地址一样)
  • client_id:表示客户端ID,必选项。

如果 code 不为 null,也就是如果是通过授权服务器重定向到这个地址来的,那么我们做如下两个操作:

1. 根据拿到的 code,去请求http://localhost:8080/oauth/token地址去获取 Token,返回的数据结构如下:

{
    "access_token": "e7f223c4-7543-43c0-b5a6-5011743b5af4",
    "token_type": "bearer",
    "refresh_token": "aafc167b-a112-456e-bbd8-58cb56d915dd",
    "expires_in": 7199,
    "scope": "all"
}
  • access_token:表示访问令牌,必选项。
  • token_type:表示令牌类型,该值大小写不敏感,必选项,可以是bearer类型或mac类型。
  • expires_in:表示过期时间,单位为秒。如果省略该参数,必须其他方式设置过期时间。
  • refresh_token:表示更新令牌,用来获取下一次的访问令牌,可选项。
  • scope:表示权限范围,如果与客户端申请的范围一致,此项可省略。

access_token 就是我们请求数据所需要的令牌,refresh_token 则是我们刷新 token 所需要的令牌,expires_in 表示 token 有效期还剩多久。

2. 接下来,根据我们拿到的 access_token,去请求资源服务器,注意 access_token 通过请求头传递,最后将资源服务器返回的数据放到 model 中。

这里我只是举一个简单的例子,目的是和把这个流程走通,正常来说,access_token 我们可能需要一个定时任务去维护,不用每次请求页面都去获取,定期去获取最新的 access_token 即可。


5、测试

首先我们去访问http://localhost:8082/index.html页面,结果如下:

然后我们点击第三方登录这个超链接,点完之后,会进入到授权服务器的默认登录页面:

接下来我们输入在授权服务器中配置的用户信息来登录,登录成功后,会看到如下页面:

在这个页面中,我们可以看到一个提示,询问是否授权 xjh 这个用户去访问被保护的资源,我们选择 approve(批准),然后点击下方的 Authorize 按钮,点完之后,页面会自动跳转回我的第三方应用中:

这个时候地址栏多了一个 code 参数,这就是授权服务器给出的授权码,拿着这个授权码,我们就可以去请求 access_token,授权码使用一次就会失效。同时大家注意到页面多了一个 Hello,这个 Hello 就是从资源服务器请求到的数据。

如本文“对您有用”,欢迎随意打赏作者,让我们坚持创作!

0 打赏
Enamiĝu al vi
不要为明天忧虑.因为明天自有明天的忧虑.一天的难处一天当就够了。
543文章 68评论 294点赞 594515浏览

随机文章
Spring Boot—整合 Thymeleaf 页面模板
5年前
RESTful风格
5年前
Java—Pair、MutablePair、ImmutablePair(实用数据结构推荐apache)
5年前
Java—数据库连接池
3年前
Spring Security—前后端分离登录,非法请求直接返回 JSON
5年前
博客统计
  • 日志总数:543 篇
  • 评论数目:68 条
  • 建站日期:2020-03-06
  • 运行天数:1927 天
  • 标签总数:23 个
  • 最后更新:2024-12-20
Copyright © 2025 网站备案号: 浙ICP备20017730号 身体没有灵魂是死的,信心没有行为也是死的。
主页
页面
  • 归档
  • 摘要
  • 杂图
  • 问题随笔
博主
Enamiĝu al vi
Enamiĝu al vi 管理员
To be, or not to be
543 文章 68 评论 594515 浏览
测试
测试
看板娘
赞赏作者

请通过微信、支付宝 APP 扫一扫

感谢您对作者的支持!

 支付宝 微信支付