单点登录系统知识点总结

单点登录系统

  • 单点登录两种解决方案
  • 系统基础服务工程设计(system)
    • 1.system服务设计结构
    • 2.系统服务之间表之间的关系。(重点)
  • 统一认证工程的设计(auth)
    • 1.auth服务设计架构
    • 2.UserDetailsServiceImpl类中的代码实现
      • (1.)pojo实体类对象User
      • (2.)负责远程调用的接口RemoteUserService
      • (3.)UserDetailsServiceImpl
    • 3.SecurityConfig代码实现
      • (1.)构建密码加密对象
      • (2.)前后端分离架构中,对成功和失败以后的信息以json形式进行返回
      • (3.)定义认证成功处理器和失败处理器
      • (4.)定义认证管理器对象,这个对象负责完成用户信息的认证
    • 3.TonkenConfig代码实现
    • 4.Oauth2Config认证授权配置代码实现
      • (1.)配置认证规则
      • (2.)通过配置对谁颁发令牌,客户端有什么特点
      • (3.)登录时要对哪个URL发起请求,通过哪个URL可以解析令牌
    • 5.生成token令牌的组成
  • 资源服务工程设计
    • 1.资源服务设计架构(resource)
  • 单点登录中常用的依赖
  • IDEA知识小点总结
      • 三级目录

单点登录两种解决方案

解决方案一:
单点登录系统知识点总结_第1张图片

客户端发送请求到网关进行验证,网关会转到auth认证授权中心,认证中心会完成用户信息的认证,完成认证会会基于UUID生成一个token,然后与用户信息绑定在一起存储到数据库.后续用户在访问资源时,基于token从数据库查询用户状态,这种方式因为要基于数据库存储和查询用户状态,所以性能表现一般.(一般可用于中小型企业,查询量较少的时候)

解决方案二:
单点登录系统知识点总结_第2张图片

用户登录成功后,会基于JWT技术生成一个token(这里的token中包含了原本存在数据库的用户信息),用户信息可以存储到这个token中.后续用户在访问资源时,对token内容解析,检查登录状态以及权限信息,无须再访问数据库.

jwt.io
工程结构设计:
因为单点登录属于多系统微服务架构,因此所有的服务开启前都需要打开nacos注册中心,将服务注册到nacosz注册中心上。
单点登录系统知识点总结_第3张图片

系统基础服务工程设计(system)

1.system服务设计结构

user包含的业务是(后续会在auth服务中进行远程调用):
1)基于用户名查询用户信息
2)基于用户id查询用户权限
Log包含的业务是:

单点登录系统知识点总结_第4张图片

2.系统服务之间表之间的关系。(重点)

单点登录系统知识点总结_第5张图片
注意:表与表之间,多对多时,关系的维护方在中间关联表。一对多时,关系的维护方在多的那一个表。
通过用户ID查询用户对应的菜单权限,首先要找到用户对应的角色,通过角色ID再到菜单表中找到对应的菜单ID,再通过菜单ID找到对应的权限。

  • 基于用户id查询用户权限,涉及到的表有:
    * 1)tb_user_roles(用户角色关系表,可以在此表中基于用户id找到用户角色)
    * 2)tb_role_menus(角色菜单关系表,可以基于角色id找到菜单id)
    * 3)tb_menus(菜单表,菜单为资源的外在表现形式,在此表中可以基于菜单id找到权限标识>
    * 基于如上三张表获取用户权限,有什么解决方案?
    * 1)方案1:三次单表查询
    * 2)方案2:嵌套查询
    * 3)方案3:多表查询

单点登录系统知识点总结_第6张图片

解决方案:

#方案一:单表查询
#基于用户id查询用户对用的角色id(查询处的角色id可能是多个)
select role_id from tb_user_roles where user_id = 1;
#基于角色id查询用户对应的菜单id
select menu_id from tb_role_menus where role_id in (1);
#基于菜单id查询菜单权限标识
select permission from tb_menus where id in (1,2,3);
#方案二:嵌套查询
select permission from tb_menus where id in (
    select menu_id from tb_role_menus where role_id in (
        select role_id from tb_user_roles where user_id = 1));
#方案三:多表查询
select m.permission
from
    tb_role_menus rm join tb_user_roles ur on rm.role_id=ur.role_id
                     join tb_menus m on rm.menu_id =m.id
where ur.role_id=1;

统一认证工程的设计(auth)

用户登陆时调用此工程对用户身份进行统一身份认证和授权

1.auth服务设计架构

单点登录系统知识点总结_第7张图片

以目标为导向进行设计,写代码时要按照正常顺序进行书写。

原来执行登录操作时的过程:request–>filter(过滤器)–>servlet–>handler intercepter–>controller
而spring.security处理请求进行过滤时就会调用AuthenticationManager——>在调用UserDetailsServiceImpl方法进行登录操作。
也就是一般基于用户名和密码进行业务操作一般写在UserDetailsService中(这是官方规定的接口),UserDetailsServiceImpl实现UserDetailsService接口,重写接口的方法。

如果单纯的是我们自己写的UserDetailsServiceImpl,是不会主动交给认证管理器,因此要实现UserDetailsService接口,并实现其方法。然后去完成密码的比对操作

1)先设计UserDetailsServiceImpl类,从sso-system服务中获取用户信息,并对用户信息进行封装返回,交给认证管理器(AuthenticationManager)。
其中要使用到远程服务调用,因此需要创建进行远程调用的接口RemoteUserService(该接口负责远程调用)。
在这里插入图片描述
当远程调用时,根据业务需求要基于用户名查询用户信息,因此需要对user进行封装,需要创建pojo实体类对象User。
2)当用户使用用户名和密码进行登录时,会将用户的密码与数据库中加密的密码进行比对,因此需要将用户传输的密码进行加密处理,需要添加配置类。
先设计一个SecurityConfig ,Bean出passwordEncoder对象用来构建密码加密对象,登录时,系统底层会基于此对象进行密码加密。
Security 认证流程分析
单点登录系统知识点总结_第8张图片
当用户发送登录请求的时候,进入到由多个过滤器构成的过滤链进行对用户和密码简单的校验,再提交到认证管理器,再通过认证管理器调用到的从数据库中取到的数据进行比对。而从数据库中取出的密码是加密的形式,而用户输入密码传到管理器不是加密的形式,不能直接比对,因此就需要在认证管理器中对用户输入进来的密码进行加密,所以要配置一个可以对密码进行加密的算法,数据库中的密码进行加密使用的算法是BCryptPasswordEncoder()的算法,因此就有了SecurityConfig配置类进行密码加密,并且使用BCry的加密算法。
底层所调用数据库数据的逻辑:
ProviderManager是AuthenticationManager认证管理器的实现类,认证管理器主要的作用就是密码的比对,身份的认证,再去调用AuthenticationProvider中的DaoAuthenticationProvider认证服务提供方,DaoAuthenticationProvider调用UserDetailsService接口中实现类的方法,主要是去访问数据库,我们自己写了一个UserDetailsServiceImpl类去实现UserDetailsService接口,重写其方法去调用数据库

单体架构的登录系统
到目前为止就是一个简单的单体架构的登录,
单点登录系统知识点总结_第9张图片
单点登录系统知识点总结_第10张图片
3)构建令牌生成及配置对象
本次我们借助JWT(Json Web Token-是一种json格式)方式将用户相关信息进行组织和加密,并作为响应令牌(Token),从服务端响应到客户端,客户端接收到这个JWT令牌之后,将其保存在客户端(例如localStorage),然后携带令牌访问资源服务器,资源服务器获取并解析令牌的合法性,基于解析结果判定是否允许用户访问资源.
因此写一个 TokenConfig类,用来配置令牌的生成,存储策略,验签方式(令牌合法性)。
单点登录系统知识点总结_第11张图片
4)构建oauth2Config配置类 负责将所有的认证和授权相关配置进行整合
业务方面:

  • 1)如何认证(认证逻辑的设计)
  • 2)认证通过以后如何颁发令牌(令牌的规范)
  • 3)为谁颁发令牌
    技术方面:
  • 1)SpringSecurity (提供认证和授权的实现)
  • 2)TokenConfig(提供了令牌的生成,存储,校验)
  • 3)Oauth2(定义了一套认证规范,例如为谁发令牌,都发什么,…)

2.UserDetailsServiceImpl类中的代码实现

(1.)pojo实体类对象User

(2.)负责远程调用的接口RemoteUserService

package com.jt.auth.service;

import com.jt.auth.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

import java.util.List;
/**
 * Feign远程调用接口,在接口中对sso-system进行远程服务调用
 * */
@FeignClient(value = "sso-system",contextId ="remoteUserService" )
public interface RemoteUserService {
    /**
     * 基于用户名查询用户信息
     * */
    @GetMapping("/user/login/{username}")
    User selectUserByUsername(@PathVariable("username") String username);
    /**
     * 基于用户id获取用户权限
     * */
    @GetMapping("/user/permission/{userId}")
    List<String> selectUserPermissions(@PathVariable("userId") Long userId);
}

(3.)UserDetailsServiceImpl

package com.jt.auth.service;

import lombok.extern.slf4j.Slf4j;
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.stereotype.Service;
import java.util.List;

@Slf4j
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    @Autowired //注入的是负责远程调用的接口
    private RemoteUserService remoteUserService;
    /**
     * 我们执行登录操作时,提交登录按钮系统会调用此方法
     * 参数:username来自客户端用户提交的用户名
     * 返回值:封装了登录用户信息以及用户权限信息的一个对象,返回的UserDetails对象最终会交给认证管理器
     * */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //1.基于用户名查找用户信息,判断用户名是否存在
        com.jt.auth.pojo.User user= remoteUserService.selectUserByUsername(username);
        if(user==null)
            throw new UsernameNotFoundException("用户不存在");
        //2.基于用户id查询用户权限(登录用户不一定可以访问所有资源)
        List<String> permissions = remoteUserService.selectUserPermissions(user.getId());
        //可以打印出有哪些权限信息
        log.info("permissions {}",permissions.toString());
        //3.封装查询结果并返回
        //这里为什么要new User(),需要查UserDetailsService接口实现类底层源码。
        return new User(user.getUsername(),
                user.getPassword(),
                AuthorityUtils.createAuthorityList(permissions.toArray(new String[]{})));
    }
}

new User()需要的三个参数:
在这里插入图片描述
在这里插入图片描述

使用AuthorityUtils.createAuthorityList()可以转化成参数所需的类型,()中里要填写的是string…数组,因此就需要将peimissions转化为数组——permissions.toArray(newString[]{})。

3.SecurityConfig代码实现

@Configuration
public class SecurityConfig  {
}

(1.)构建密码加密对象

	//构建密码加密对象,登录时,系统底层会基于此对象进行密码加密
	@Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

(2.)前后端分离架构中,对成功和失败以后的信息以json形式进行返回

要继承WebSecurityConfigurerAdapter,重写其中的configure(HttpSecurity http)方法——这个方法会在服务启动时就会启动,进行自定义的配置

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
	@Override
    protected void configure(HttpSecurity http) throws Exception{
        //super.configure(http);//默认所有请求都要认证,不需要
        //1.禁用跨域攻击(先这么写,不写会报403异常,这里的登录默认是post请求,但系统底层的跨域设计是不允许post请求,)
        http.csrf().disable();
        //2.放行所有资源的访问(后续可以基于选择对资源进行认证和放行)此处是根据业务需求进行选择
        //3.自定义登录成功和失败以后的处理逻辑(目的是响应到客户端的数据是json字符串)
        //默认没有配置,会跳转到登录成功或失败的页面,实际上前后端分离架构中服务端不负责页面跳转,只负责返回json数据
        http.formLogin()//登录表单,此方法执行后会生成一个/login的url
                //登录成功处理器successHandler
                .successHandler(successHandler())//启动时就会调用,并不会登录成功才调用
                .failureHandler(failureHandler());
    }
}

放行资源访问的方式
单点登录系统知识点总结_第12张图片
anyRequest()所有的请求路径

(3.)定义认证成功处理器和失败处理器

//定义认证成功处理器
    //登录成功以后返回json数据
    @Bean
    public AuthenticationSuccessHandler successHandler() {
        return (request, response, authentication) -> {
            //定义响应数据
            Map<String,Object> map = new HashMap<>();
            map.put("state",200);
            map.put("message","login success");
            //将响应数据写到客户端,调用writeJsonToClient方法
            writeJsonToClient(response,map);
        };
    }
    //定义登录失败处理器
    @Bean
    public AuthenticationFailureHandler failureHandler() {
        return (request,response,exception)->{
            //构建map对象封装到要响应到客户端的数据
            Map<String,Object> map=new HashMap<>();
            map.put("state",500);
            map.put("message", "login error");
            //将map对象转换为json格式字符串并写到客户端
            writeJsonToClient(response,map);
        };
    }
    private void writeJsonToClient(
            HttpServletResponse response,
            Map<String,Object> map) throws IOException {
        //将要响应到客户端的数据转化成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();//刷新
    }

(4.)定义认证管理器对象,这个对象负责完成用户信息的认证

即判定用户身份信息的合法性,在基于oauth2协议完成认证时,需要此对象,所以这里将此对象拿出来交给spring管理

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

3.TonkenConfig代码实现

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 {

    /**
     * 定义令牌存储的方案,本次选择基于JWT令牌方式存储用户状态
     * */
    @Bean
    public TokenStore tokenStore(){
        //这里采用JWT方式生成和存储令牌信息
        return new JwtTokenStore(jwtAccessTokenConverter());
    }
    /**
     * 配置JWT令牌和解析对象
     * */
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey(SIGNING_KEY);//设置密钥
        return converter;
    }
    /**
     * JWT 令牌签名时使用的密钥(可以理解为盐值加密中的盐)
     * 1)生成的令牌需要这个密钥进行签名
     * 2)获取的令牌需要使用这个密钥进行验签(校验令牌合法性,是否被篡改过)
     */
    //这里的签名key将来可以写到配置中心
    private static final String SIGNING_KEY="auth";
}

4.Oauth2Config认证授权配置代码实现

适配器模式:不需要重写方法,适配器充当一个转化的作用,需要哪个方法,在进行重写。

//AuthorizationServerConfigurerAdapter认证授权的适配器  ——用到了适配器模式
//因此Oauth2Config继承这个接口,不需要重写全部的方法,需要哪个方法在进行定义即可
@AllArgsConstructor //生成一个全参构造函数
@EnableAuthorizationServer //启动认证和授权
@Configuration
public class Oauth2Config extends AuthorizationServerConfigurerAdapter {
	//为下文需要调用的进行注入。
	//1.可以给每个上面都加入@Autowired
	//2.加入一个全参构造函数,或者在类上面加一个@AllArgsConstructor //生成一个全参构造函数
    private AuthenticationManager authenticationManager;//在SecurityConfig  @Bean一个authenticationManager()
    private UserDetailsServiceImpl userDetailsService;//调用的是service层中的UserDetailsServiceImpl类
    private TokenStore tokenStore;//在TokenConfig  @Bean tokenStore()
    private PasswordEncoder passwordEncoder;//在SecurityConfig @Bean  passwordEncoder()加密的算法
    private JwtAccessTokenConverter jwtAccessTokenConverter;//在TokenConfig @Bean
}

(1.)配置认证规则

@Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        //super.configure(endpoints);
        endpoints
                //配置由谁完成认证?(认证管理器)
                .authenticationManager(authenticationManager)
                //谁负责查询用户业务数据(可写可不写。认证时需要两部分信息:一部分来自客户端,一部分来自数据库)
                .userDetailsService(userDetailsService)
                //支持对什么请求进行认证(可写可不写。默认支持post方式)
                .allowedTokenEndpointRequestMethods(HttpMethod.GET,HttpMethod.POST)
                //认证成功以后令牌如何生成和存储?(若不写默认生成普通令牌UUID.randomUUID(),存储方式为内存)
                .tokenServices(tokenService());
    }
    @Bean
    public AuthorizationServerTokenServices tokenService() {
        //1.创建授权令牌服务对象(TokenServices)
        DefaultTokenServices tokenServices =  new DefaultTokenServices();
        //2.配置令牌的存储(tokenStore)
        tokenServices.setTokenStore(tokenStore);
        //3.配置令牌增强(不进行配置,就是默认普通令牌,使用的就是UUID)
        TokenEnhancerChain tokenEnhancer=new TokenEnhancerChain();
        tokenEnhancer.setTokenEnhancers(Arrays.asList(jwtAccessTokenConverter));
        tokenServices.setTokenEnhancer(tokenEnhancer);
        //4.设置令牌的有效时间
        tokenServices.setAccessTokenValiditySeconds(3600);//1小时
        //5.设置令牌刷新策略(是否支持使用刷新令牌在生成新令牌)
        tokenServices.setSupportRefreshToken(true);
        //6.设置刷新令牌有效时长
        tokenServices.setRefreshTokenValiditySeconds(3600*5);//5小时
        return tokenServices;
    }

(2.)通过配置对谁颁发令牌,客户端有什么特点

@Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        //super.configure(clients);
        clients.inMemory()
                //定义客户端要携带的id(客户端访问此服务时要携带的id,这个是自己定义的字符串)
                .withClient("gateway-client")
                //定义客户端要携带的秘钥(这个秘钥也 是官方定义的一个规则,客户端要携带,字符串可与自己)
                .secret(passwordEncoder.encode("123456"))
                //定义作用范围(所有符合定义规则的客户端,例如:client、secret。。。)
                .scopes("all")
                //定义允许的认证方式(基于密码和刷新令牌的方式实现认证)
                .authorizedGrantTypes("password","refresh_token");
    }

(3.)登录时要对哪个URL发起请求,通过哪个URL可以解析令牌

    //配置要对外暴露的认证URL,刷新令牌的URL,检查令牌的URL
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        //super.configure(security);
        //1.定义(公开)要认证的url(permitAll()是官方定义好的)
        //公开oauth/token_key端点
        security.tokenKeyAccess("permitAll()")
                //公开检查token有效性的URL
                .checkTokenAccess("permitAll()")
                //允许通过表单提交方式进行认证
                .allowFormAuthenticationForClients();
    }

5.生成token令牌的组成

单点登录系统知识点总结_第13张图片
上述的固定参数key要和以下配置类中定义的数据保持一致,否则会出现401错误
单点登录系统知识点总结_第14张图片

资源服务工程设计

1.资源服务设计架构(resource)

资源服务工程为一个业务数据工程,此工程中数据在访问通常情况下是受限访问,例如有些资源有用户,可以访问,有些资源必须认证才可访问,有些资源认证后,有权限才可以访问。
单点登录系统知识点总结_第15张图片

用户访问资源时的认证,授权流程设计如下
单点登录系统知识点总结_第16张图片
如何设计对这个资源工程的访问?

  • 1.有些资源可以直接访问,无需认证
  • 2.有些资源必须认证以后才可以访问
  • 3.有些资源认证以后,还必须有权限才可以访问

单点登录中常用的依赖

1.feign进行远程调用

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

2.nacos 服务注册

        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

3.Nacos 服务配置

        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>

4.SSO技术方案:Spring Security+JWT+oauth2
该依赖中包含Spring Security(安全)、actuator(安全检查)、Spring Security jwt(生成令牌)、Spring Security oauth2(安全协议、定义一个规范)的依赖。

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>

资源服务器中添加该依赖只做授权,不做认证,添加完此依赖以后,在项目中我们要做哪些事情?对受限访问的资源可以先判断是否登录了,已经认证用户还要判断是否有权限?
5.Spring Web依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

IDEA知识小点总结

1.Pojo实现类中自动生成UID——序列化ID的唯一标识
单点登录系统知识点总结_第17张图片
2.在IDEA中打开数据库并管理表信息
单点登录系统知识点总结_第18张图片

三级目录

你可能感兴趣的:(微服务,java,spring,cloud)