Spring Cloud 微服务安全 | (五) 实现授权码登录_Spring Security

Author:Hedon

Github:spring-security-oauth2.0

前篇:

Spring Cloud 微服务安全 | (一) API 安全

Spring Cloud 微服务安全 | (二) 网关安全

Spring Cloud 微服务安全 | (三) 登录功能
Spring Cloud 微服务安全 | (四) OAuth2.0 的四种授权模式


1. 前面系统遗留的问题

先来看前面系统的架构:

Spring Cloud 微服务安全 | (五) 实现授权码登录_http_02

  1. 用户的用户名和密码都交给了前端服务器,这样就会有一些安全隐患。因为每个前端服务器的开发人员都有可能接触到用户的用户名和密码;
  2. 每个客户端应用都需要去处理登录逻辑,一旦登录逻辑改变了,牵扯到的地方就非常多,也就是耦合。

2. 改进的系统架构

Spring Cloud 微服务安全 | (五) 实现授权码登录_Spring Security_03

我们现在希望浏览器直接向认证服务器去发送认证请求,这样:

  1. 前端服务器就接触不到用户的用户名和密码;
  2. 登录逻辑不在前端服务器上执行。

3. 系统重构

3.1 废弃登录界面

我们现在根本不需要登录页面了。因为如果用户没登录的话,我们会直接将其跳转到认证服务器上去认证(其实这个时候 Spring Security 会有一个默认的登录界面的,你的 username 和 password 的总归还是要提交的)。

Spring Cloud 微服务安全 | (五) 实现授权码登录_http_04

3.2 跳转到认证服务器

我们进入 index.html 界面的时候,会先 checkLogin(),这里要修改一下,如果没有登录的话,直接跳转到认证服务器去认证:

//检查是否已经登录
function checkLoginOrNot() {
    $.ajax({
        async:false,
        url:"/checkLogin",
        type:"POST",
        success:function (result) {
            if (result.code=="00000"){
                $("#loginMsg").text("Hello "+result.message + "!! You've been authenticated!!");
            } else{
                $("#loginMsg").text("Please Login in!");
                //跳转到认证服务器上
                window.location.href = "http://localhost:9070/oauth/authorize?"+
                        "client_id=frontEnd"+"&"+
                        "redirect_uri=http://localhost:9060/oauth/callback"+"&"+
                        "response_type=code"+"&"+
                  			"state=abc";
            }
        }
    })
}

认证服务器认证的路径是:/oauth/authorize,必须要携带 3 个参数:

  • client_id:客户端 ID。
  • redirect_uri:重定向 URI,可以是认证成功后要跳转到哪个界面,也可以是回调接口,这里我们回调到前端服务器的 oauth/callback 接口,后面再实现这个接口。
  • response_type:这里的 code 是说要返回一个授权码

可再携带一个可选参数:

  • state:这个字段可以记录一下自定义的信息,比如是从哪个界面跳到认证界面的,我们可以根据这个信息进行一些自定义的操作。

3.3 实现回调接口

下面在 front-endUserController 来实现 /oauth/callback 接口:

/**
 * 回调接口
 *
 * @param code         认证服务器返回的授权码,必须
 * @param state        我们自定义的返回的客户端状态信息,可选
 * @param request
 */
@GetMapping("/oauth/callback")
public void callBack(@RequestParam("code") String code, String state, HttpServletRequest request, HttpServletResponse response) throws IOException {
    //日志输出 state 状态信息
    log.info("state is "+ state);
    //去认证服务器拿token,还是走网关
    String oauthServiceUrl = "http://localhost:9527/token/oauth/token";
    //请求头
    HttpHeaders headers = new HttpHeaders();
    headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
    headers.setBasicAuth("frontEnd","123456");
    /**
     * 参数:
     * 1. 授权码
     * 2. 授权模式:授权码模式authorization_code
     * 3. 重定向 uri:要跟前端的一样
     */
    MultiValueMap<String,String> params = new LinkedMultiValueMap<>();
    params.add("code",code);
    params.add("grant_type","authorization_code");
    params.add("redirect_uri","http://localhost:9060/oauth/callback");
    //封装请求体
    HttpEntity<MultiValueMap<String,String>> entity = new HttpEntity<>(params,headers);
    //发送请求
    ResponseEntity<TokenInfo> token = restTemplate.exchange(oauthServiceUrl, HttpMethod.POST, entity, TokenInfo.class);
    //存到 session 中
    request.getSession().setAttribute("token",token.getBody());
    //跳转回浏览器
    response.sendRedirect("/index");
}

3.4 修改认证服务器的客户端信息

这里我们加入授权码模式,所以需要在数据修改 oauth_client_details 表,来让 frontEnd 这个客户端支持授权码模式:

  1. 加入授权码模式
  2. 持久化重定向 URI,更安全

Spring Cloud 微服务安全 | (五) 实现授权码登录_Java学习_05

4. 测试

我们直接访问:localhost:9060/index

因为这个时候我们没有登录,所以会转发到认证服务器上去认证,拿授权码,所以这里会跳转到 Spring Security 自带的登录界面,当然我们也可以自定义这个登录界面。

Spring Cloud 微服务安全 | (五) 实现授权码登录_JavaEE_06

我们现在来进行登录:

Spring Cloud 微服务安全 | (五) 实现授权码登录_JavaEE_07

登录后就跳转到了上面这个界面。这说明我们已经认证成功了,现在这个界面是授权界面。我们可以授权 scope 字段中有的权限。现在我们是手动授权,其实我们也可以修改数据库中的 oauth_client_details 中的 autoapprove 字段,来实现自动授权:

如果是 true 的话,就默认自动授权所有权限:

Spring Cloud 微服务安全 | (五) 实现授权码登录_java_08

也可以设置为 read,只授权指定的若干种权限。

我们点击 Authorize 进行授权,授权后就跳转到我们的 index.html 页面了:

Spring Cloud 微服务安全 | (五) 实现授权码登录_JavaEE_09

并且这个时候前端服务器已经携带授权码 code 去认证服务器拿好 token 了,这个时候 session 中有 token,我们可以获取订单信息。

5. 流程图

Spring Cloud 微服务安全 | (五) 实现授权码登录_http_10

6. 一些细节

6.1 为什么会跳转到内置的 login 界面

认证服务器 auth-service 中配置网络安全配置类的时候,继承了 WebSecurityConfigurerAdapter 这个类:

@Configuration
@EnableWebSecurity  //支持 WebSecurity
public class OAuth2WebSecurityConfig extends WebSecurityConfigurerAdapter {

它的 configure(HttpSecurity http) 方法有个默认实现如下:

Spring Cloud 微服务安全 | (五) 实现授权码登录_http_11

正是基于以上 2 个默认设置,所以才会跳转到内置的 login 界面。

6.2 为什么数据库里面也要存 redirect_uri

如果不存的话,黑客就机可乘了。他们可能通过某种操作拿到 code 后重定向到钓鱼网站里面,这样就不安全了。

数据库存储 redirect_uri 的话,在重定向的时候,就会对比参数中的 redirect_uri 和数据库中的是否一致,如果一致,那么就没问题,如果不一致,就会报错,重定向失败,从而保证安全。