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

x33g5p2x  于2021-09-26 转载在 其他  
字(9.3k)|赞(0)|评价(0)|浏览(281)

简介

背景分析

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

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

单点登陆系统

单点登录,英文是 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.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;

import javax.security.auth.AuthPermission;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;

/** * spring security 配置类,此类中要配置: * 1)加密对象 * 2)配置认证规则 * 当我们在执行登录操作时,底层逻辑(了解): * 1)Filter(过滤器) * 2)AuthenticationManager (认证管理器) * 3)AuthenticationProvider(认证服务处理器) * 4)UserDetailsService(负责用户信息的获取及封装) */
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    //初始化加密对象
    //此对象提供了一种不可逆的加密方式,相对于md5方式会更加安全
    @Bean
    public BCryptPasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
    /**配置认证规则*/
    @Override
    protected void configure(HttpSecurity http)
            throws Exception {
        //super.configure(http);
        //1.禁用跨域攻击(先这么写)
        http.csrf().disable();
        //2.放行所有资源的访问
        http.authorizeRequests().anyRequest().permitAll();

        //必须先认证才能访问
        //http.authorizeRequests().antMatchers("/other/addcart").authenticated();

        //3.定义登录成功和失败以后的处理逻辑(可选)
        //假如没有如下设置登录成功会显示404
        http.formLogin().successHandler(successHandler())
                .failureHandler(failureHandler());
    }
    //定义认证成功处理器
    //登录成功以后返回json数据
    @Bean
    public AuthenticationSuccessHandler successHandler(){
            /*return new AuthenticationSuccessHandler(){ @Override public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException { } };*/
        //lambda
        return(request,response,authentication)->{
            //1.构建map对象封装到要响应到客户端的数据
            Map<String,Object> map = new HashMap<>();
            map.put("state", 200);
            map.put("message", "login ok");
            //2.将map对象转换为json格式字符串并写到客户端
            writeJsonToClient(response,map);
        };
    }
    //定义认证失败处理器
    @Bean
    public AuthenticationFailureHandler failureHandler(){
            /*return new AuthenticationFailureHandler(){ @Override public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { } };*/
        return(request,response,exception)->{
            //1.构建map对象封装到要响应到客户端的数据
            Map<String,Object> map = new HashMap<>();
            map.put("state", 500);
            map.put("message", "login failure");
            //2.将map对象转换为json格式字符串并写到客户端
            writeJsonToClient(response,map);
        };
    }
    private void writeJsonToClient(HttpServletResponse response,Map<String,Object> map) throws IOException {
        //将map对象转换为json
        String json=new ObjectMapper().writeValueAsString(map);
        //设置响应数据的编码格式
        response.setCharacterEncoding("utf-8");
        //设置响应数据的类型
        response.setContentType("application/json;charset=utf-8");
        //设置响应数据到客户端
        PrintWriter out = response.getWriter();
        out.println(json);
        out.flush();

    }
}

定义用户信息处理对象

在spring security应用中底层会借助UserDetailService对象获取数据库信息,并进行封装,最后返回给认证管理器,完成认证操作,例如:

package com.jt.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
/** * 登录时用户信息的获取和封装会在此对象进行实现, * 在页面上点击登录按钮时,会调用这个对象的loadUserByUsername方法, * 页面上输入的用户名会传给这个方法的参数 */
@Service
public class UserDetailsServiceImpl
        implements UserDetailsService {
    @Autowired
    private BCryptPasswordEncoder passwordEncoder;
    //UserDetails用户封装用户信息(认证和权限信息)
    
    
    /** * 基于用户名获取数据库中的用户信息 * @param username 这个username来自客户端 * @return * @throws UsernameNotFoundException */
    @Override
    public UserDetails loadUserByUsername(String username)
            throws UsernameNotFoundException {
        //1.基于用户名查询用户信息(暂时先给假数据)
        //Userinfo info=userMapper.selectUserByUsername(username);
        String encodePassword=//假设这个密码来自数据库
                passwordEncoder.encode("123456");
        //2.封装用户相关信息(用户名,密码,用户权限信息等)并返回
        return new User(username,
                encodePassword,//必须是已经加密的密码
                AuthorityUtils.createAuthorityList(//这里的权限后面讲
                        "sys:res:create",
                        "sys:res:retrieve"));
    }
}

网关中登陆路由配置

在网关配置文件中添加登录路由配置,例如

- 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),然后携带令牌访问资源服务器,资源服务器获取并解析令牌的合法性,基于解析结果判定是否允许用户访问资源.

相关文章