09-微服务版的单点登陆系统设计及实现

Posted 雨田说码

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了09-微服务版的单点登陆系统设计及实现相关的知识,希望对你有一定的参考价值。

简介

背景分析

传统的登录系统中,每个站点都实现了自己的专用登录模块。各站点的登录状态相互不认可,各站点需要逐一手工登录。例如:

这样的系统,我们又称之为多点登陆系统。应用起来相对繁琐(每次访问资源服务都需要重新登陆认证和授权)。与此同时,系统代码的重复也比较高。由此单点登陆系统诞生。

单点登陆系统

单点登录,英文是 Single Sign On(缩写为 SSO)。即多个站点共用一台认证授权服务器,用户在其中任何一个站点登录后,可以免登录访问其他所有站点。而且,各站点间可以通过该登录状态直接交互。例如:

快速入门实践

工程结构如下:

创建认证授权工程

添加项目依赖

  <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-oauth2</artifactId>
    </dependency>
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
    </dependency>

构建项目配置文件

在sca-auth工程中创建bootstrap.yml文件,例如:

server:
  port: 8071
spring:
  application:
    name: sca-auth
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
      config:
        server-addr: localhost:8848

添加项目启动类

package com.jt;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

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

启动并访问项目

项目启动时,系统会默认生成一个登陆密码,例如:


打开浏览器输入http://localhost:8071呈现登陆页面,例如:


其中,默认用户名为user,密码为系统启动时,在控制台呈现的密码。执行登陆测试,登陆成功进入如下界面(因为没有定义登陆页面,所以会出现404):

自定义登陆逻辑

业务描述

我们的单点登录系统最终会按照如下结构进行设计和实现,例如:

我们在实现登录时,会在UI工程中,定义登录页面(login.html),然后在页面中输入自己的登陆账号,登陆密码,将请求提交给网关,然后网关将请求转发到auth工程,登陆成功和失败要返回json数据,在这个章节我们会按这个业务逐步进行实现

定义安全配置类

修改SecurityConfig配置类,添加登录成功或失败的处理逻辑,例如:

package com.jt.auth.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    /**初始化密码加密对象*/
    @Bean
    public BCryptPasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
    /**在这个方法中定义登录规则
     * 1)对所有请求放行(当前工程只做认证)
     * 2)登录成功信息的返回
     * 3)登录失败信息的返回
     * */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //关闭跨域工具
        http.csrf().disable();
        //放行所有请求
        http.authorizeRequests().anyRequest().permitAll();
        //登录成功与失败的处理
        http.formLogin()
                .successHandler(successHandler())
                .failureHandler(failureHandler());
    }

    @Bean
    public AuthenticationSuccessHandler successHandler(){
//        return new AuthenticationSuccessHandler() {
//            @Override
//            public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
//
//            }
//        }
        return (request,response,authentication) ->{
            //1.构建map对象,封装响应数据
            Map<String,Object> map=new HashMap<>();
            map.put("state",200);
            map.put("message","login ok");
            //2.将map对象写到客户端
            writeJsonToClient(response,map);
        };
    }
    @Bean
    public AuthenticationFailureHandler failureHandler(){
        return (request,response, e)-> {
            //1.构建map对象,封装响应数据
            Map<String,Object> map=new HashMap<>();
            map.put("state",500);
            map.put("message","login failure");
            //2.将map对象写到客户端
            writeJsonToClient(response,map);
        };
    }
    private void writeJsonToClient(HttpServletResponse response,
                                   Object object) throws IOException {
        //1.将对象转换为json
        //将对象转换为json有3种方案:
        //1)Google的Gson-->toJson  (需要自己找依赖)
        //2)阿里的fastjson-->JSON (spring-cloud-starter-alibaba-sentinel)
        //3)Springboot web自带的jackson-->writeValueAsString (spring-boot-starter-web)
        //我们这里借助springboot工程中自带的jackson
        //jackson中有一个对象类型为ObjectMapper,它内部提供了将对象转换为json的方法
        //例如:
        String jsonStr=new ObjectMapper().writeValueAsString(object);
        //3.将json字符串写到客户端
        PrintWriter writer = response.getWriter();
        writer.println(jsonStr);
        writer.flush();
    }
}

定义用户信息处理对象

package com.jt.auth.service;
@Service
public class UserDetailServiceImpl implements UserDetailsService {
    @Autowired
    private BCryptPasswordEncoder passwordEncoder;
    /**
     * 当我们执行登录操作时,底层会通过过滤器等对象,调用这个方法.
     * @param username 这个参数为页面输出的用户名
     * @return 一般是从数据库基于用户名查询到的用户信息
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username)
            throws UsernameNotFoundException {
        //假设用户名就是用户输入的用户名(username),这个密码是基于用户名从数据库查询到的
        String encodedPwd=passwordEncoder.encode("123456");
        //假设这个权限信息也是从数据库查询到的
        List<GrantedAuthority> grantedAuthorities =AuthorityUtils.commaSeparatedStringToAuthorityList(
                "sys:res:retrieve,sys:res:create");
        //这个user是SpringSecurity提供的UserDetails接口的实现,用于封装用户信息
        //后续我们也可以基于需要自己构建UserDetails接口的实现
        User user=new User(username,encodedPwd,grantedAuthorities);
        return user;
    }
}

网关中登陆路由配置

在网关配置文件中添加如下配置:

 - id: router02
   uri: lb://sca-auth  #lb表示负载均衡,底层默认使用ribbon实现
   predicates: #定义请求规则(请求需要按照此规则设计)
      - Path=/auth/login/** #请求路径设计
   filters:
      - StripPrefix=1 #转发之前去掉path中第一层路径

基于Postman进行访问测试

启动sca-gateway,sca-auth服务,然后基于postman进行访问测试,例如:

自定义登陆页面

在sca-resource-ui工程的static目录中定义登陆页面,例如:

<!doctype html>
<html lang="en">
<head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <!-- Bootstrap CSS -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
    <title>login</title>
</head>
<body>
<div class="container"id="app">
    <h3>Please Login</h3>
    <form>
        <div class="mb-3">
            <label for="usernameId" class="form-label">Username</label>
            <input type="text" v-model="username" class="form-control" id="usernameId" aria-describedby="emailHelp">
        </div>
        <div class="mb-3">
            <label for="passwordId" class="form-label">Password</label>
            <input type="password" v-model="password" class="form-control" id="passwordId">
        </div>
        <button type="button" @click="doLogin()" class="btn btn-primary">Submit</button>
    </form>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script>
    var vm=new Vue({
        el:"#app",//定义监控点,vue底层会基于此监控点在内存中构建dom树
        data:{ //此对象中定义页面上要操作的数据
            username:"",
            password:""
        },
        methods: {//此位置定义所有业务事件处理函数
            doLogin() {
                //1.定义url
                let url = "http://localhost:9000/auth/login"
                //2.定义参数

                let params = new URLSearchParams()
                params.append('username',this.username);
                params.append('password',this.password);
                //3.发送异步请求
                axios.post(url, params).then((response) => {
                    debugger
                    let result=response.data;
                    console.log(result);
                    if (result.state == 200) {
                        alert("login ok");
                    } else {
                        alert(result.message);
                    }
                })
            }
        }
    });
</script>
</body>
</html>

启动sca-resource-ui服务后,进入登陆页面,输入用户名jack,密码123456进行登陆测试。

颁发登陆成功令牌

构建令牌配置对象

本次我们借助JWT(Json Web Token-是一种json格式)方式将用户相关信息进行组织和加密,并作为响应令牌(Token),从服务端响应到客户端,客户端接收到这个JWT令牌之后,将其保存在客户端(例如localStorage),然后携带令牌访问资源服务器,资源服务器获取并解析令牌的合法性,基于解析结果判定是否允许用户访问资源.

package com.jt.auth.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;

@Configuration
public class TokenConfig {
    //解密口令
     private String SIGNING_KEY = "auth";

    //构建令牌生成器对象
    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(jwtAccessTokenConverter());
    }
    //Jwt转换器,将任何数据转换为jwt字符串
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter(){
        JwtAccessTokenConverter converter=new JwtAccessTokenConverter();
        //设置加密/解密口令
        converter.setSigningKey(SIGNING_KEY);
        return converter;
    }

}

定义认证授权核心配置

第一步:在SecurityConfig中添加如下方法(创建认证管理器对象,后面授权服务器会用到):

 @Bean
    public AuthenticationManager authenticationManagerBean()
            throws Exception {
        return super.authenticationManagerBean();
    }

第二步:所有零件准备好了开始拼装最后的主体部分,这个主体部分就是授权服务器的核心配置

package com.cy.jt.auth.config;

import lombok.AllArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.以上是关于09-微服务版的单点登陆系统设计及实现的主要内容,如果未能解决你的问题,请参考以下文章

单点登录实现方案

10-单点登录系统拓展实现(自己尝试实现)

附代码单点登录介绍和服务端实现

单点登录实现原理和单点登录服务器设计

单点登录实现原理和单点登录服务器设计

nodejs+koa+uniapp实现微信小程序登陆获取用户手机号及openId