第5章 认证授权:需求分析,Security介绍(OAuth2,JWT),用户认证,微信扫码登录,用户授权

1 模块需求分析

1.1 什么是认证授权

截至目前,项目已经完成了课程发布功能,课程发布后用户通过在线学习页面点播视频进行学习。如何去记录学生的学习过程呢?要想掌握学生的学习情况就需要知道用户的身份信息,记录哪个用户在什么时间学习什么课程,如果用户要购买课程也需要知道用户的身份信息。所以,去管理学生的学习过程最基本的要实现用户的身份认证。

认证授权模块实现平台所有用户的身份认证与用户授权功能。

什么是用户身份认证?

  • 用户身份认证即用户去访问系统资源时系统要求验证用户的身份信息,身份合法方可继续访问。常见的用户身份认证的表现形式有:用户名密码登录,微信扫码等方式。
  • 项目包括学生、学习机构的老师、平台运营人员三类用户,不管哪一类用户在访问项目受保护资源时都需要进行身份认证。比如:发布课程操作,需要学习机构的老师首先登录系统成功,然后再执行发布课程操作。创建订单,需要学生用户首先登录系统,才可以创建订单。如下图:
    在这里插入图片描述

什么是用户授权?

  • 用户认证通过后去访问系统的资源,系统会判断用户是否拥有访问资源的权限,只允许访问有权限的系统资源,没有权限的资源将无法访问,这个过程叫用户授权。比如:用户去发布课程,系统首先进行用户身份认证,认证通过后继续判断用户是否有发布课程的权限,如果没有权限则拒绝继续访问系统,如果有权限则继续发布课程。如下图:
    在这里插入图片描述

1.2 业务流程

1.2.1 统一认证

项目包括学生、学习机构的老师、平台运营人员三类用户,三类用户将使用统一的认证入口,如下图:
在这里插入图片描述

用户输入账号和密码提交认证,认证通过则继续操作。

项目由统一认证服务受理用户的认证请求,如下图:
在这里插入图片描述

认证通过由认证服务向给用户颁发令牌,相当于访问系统的通行证,用户拿着令牌去访问系统的资源。

1.2.2 单点登录

本项目基于微服务架构构建,微服务包括:内容管理服务、媒资管理服务、学习中心服务、系统管理服务等,为了提高用户体验性,用户只需要认证一次便可以在多个拥有访问权限的系统中访问,这个功能叫做单点登录。

引用百度百科:单点登录(Single Sign On),简称为 SSO,是目前比较流行的企业业务整合的解决方案之一。SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。

如下图,用户只需要认证一次,便可以在多个拥有访问权限的系统中访问。

在这里插入图片描述

1.2.3 第三方认证

为了提高用户体验,很多网站有扫码登录的功能,如:微信扫码登录、QQ扫码登录等。扫码登录的好处是用户不用输入账号和密码,操作简便,另外一个好处就是有利于用户信息的共享,互联网的优势就是资源共享,用户也是一种资源,对于一个新网站如果让用户去注册是很困难的,如果提供了微信扫码登录将省去用户注册的成本,是一种非常有效的推广手段。

微信扫码登录其中的原理正是使用了第三方认证,如下图:
在这里插入图片描述

2 Spring Security 认证研究

2.1 Spring Security介绍

认证功能几乎是每个项目都要具备的功能,并且它与业务无关,市面上有很多认证框架,如:Apache Shiro、CAS、Spring Security等。由于本项目基于Spring Cloud技术构建,Spring Security是spring家族的一份子且和Spring Cloud集成的很好,所以本项目选用Spring Security作为认证服务的技术框架。

Spring Security 是一个功能强大且高度可定制的身份验证和访问控制框架,它是一个专注于为 Java 应用程序提供身份验证和授权的框架。

项目主页:https://spring.io/projects/spring-security

Spring cloud Security: https://spring.io/projects/spring-cloud-security

2.2 认证授权入门

2.2.1 创建认证服务工程

下边我们使用Spring Security框架快速构建认证授权功能体系。

1、部署认证服务工程

从课程资料中拷贝xuecheng-plus-auth工程到自己的工程目录下。

此工程是一个普通的spring boot工程,可以连接数据库。

此工程不具备认证授权的功能。

2、创建数据库

创建xc402_users数据库

导入课程资料中的xcplus_users.sql脚本。
在这里插入图片描述

修改为自己的命名空间
在这里插入图片描述

在nacos中新增auth-service-dev.yaml:
在这里插入图片描述

server:
  servlet:
    context-path: /auth
  port: 63070
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://192.168.101.65:3306/xc402_users?serverTimezone=UTC&userUnicode=true&useSSL=false&
    username: root
    password: mysql

初始工程自带了一个Controller类,如下:

package com.xuecheng.auth.controller;


/**
 * @author Mr.M
 * @version 1.0
 * @description 测试controller
 * @date 2022/9/27 17:25
 */
@Slf4j
@RestController
public class LoginController {

  @Autowired
  XcUserMapper userMapper;

  @RequestMapping("/login-success")
  public String loginSuccess(){

      return "登录成功";
  }


  @RequestMapping("/user/{id}")
  public XcUser getuser(@PathVariable("id") String id){
    XcUser xcUser = userMapper.selectById(id);
    return xcUser;
  }

  @RequestMapping("/r/r1")
  public String r1(){
    return "访问r1资源";
  }

  @RequestMapping("/r/r2")
  public String r2(){
    return "访问r2资源";
  }



}

启动工程,尝试访问http://localhost:63070/auth/r/r1 :
在这里插入图片描述

访问用户信息:http://localhost:63070/auth/user/52
在这里插入图片描述

以上测试一切正常说明此工程部署成功。

2.2.2 认证测试

下边向auth认证工程集成Spring security,向pom.xml加入Spring Security所需要的依赖

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

重启工程,访问http://localhost:63070/auth/r/r1

自动进入/login登录页面,/login是spring security提供的,此页面有几个css样式加载会稍微慢点,如下图:
在这里插入图片描述

账号和密码是多少呢?下一步需要进行安全配置。

拷贝课程资料下的WebSecurityConfig.java到config下需要三部分内容:
1、用户信息

  • 在内存配置两个用户:zhangsan、lisi
  • zhangsan用户拥有的权限为p1
  • lisi用户拥有的权限为p2

2、密码方式

  • 暂时采用明文方式

3、安全拦截机制

  • /r/**开头的请求需要认证
  • 登录成功到成功页面

代码如下:

在这里插入图片描述

//配置用户信息服务
@Bean
public UserDetailsService userDetailsService() {
    //这里配置用户信息,这里暂时使用这种方式将用户存储在内存中
    InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
            manager.createUser(User.withUsername("zhangsan").password("123").authorities("p1").build());
    manager.createUser(User.withUsername("lisi").password("456").authorities("p2").build());
    return manager;
}

    @Bean
    public PasswordEncoder passwordEncoder() {
        //密码为明文方式
        return NoOpPasswordEncoder.getInstance();
    }

    //配置安全拦截机制
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers("/r/**").authenticated()//访问/r开始的请求需要认证通过
                .anyRequest().permitAll()//其它请求全部放行
                .and()
                .formLogin().successForwardUrl("/login-success");//登录成功跳转到/login-success
                http.logout().logoutUrl("/logout");//退出地址
    }

重启工程

1、访问http://localhost:63070/auth/user/52 可以正常访问
在这里插入图片描述

2、访问http://localhost:63070/auth/r/r1 显示登录页面
在这里插入图片描述

账号zhangsan,密码为123,如果输入的密码不正确会认证失败,输入正确显示登录成功。
在这里插入图片描述
在这里插入图片描述
登录成功后再次访问,即可访问成功。
在这里插入图片描述

为什么http://localhost:63070/auth/user/52 可以正常访问,访问http://localhost:63070/auth/r/r1 显示登录页面?

http.logout().logoutUrl(“/logout”):配置了退出页面,认证成功后访问/logout可退出登录。
在这里插入图片描述

2.2.3 授权测试

用户认证通过去访问系统资源时spring security进行授权控制,判断用户是否有该资源的访问权限,如果有则继续访问,如果没有则拒绝访问。

下边测试授权功能:

1、配置用户拥有哪些权限。

在WebSecurityConfig类配置zhangsan拥有p1权限,lisi拥有p2权限。

@Bean
public UserDetailsService userDetailsService() {
    //这里配置用户信息,这里暂时使用这种方式将用户存储在内存中
    InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
    manager.createUser(User.withUsername("zhangsan").password("123").authorities("p1").build());
    manager.createUser(User.withUsername("lisi").password("456").authorities("p2").build());
    return manager;
}

2、指定资源与权限的关系。

什么是系统的资源?

比如:查询一个用户的信息,用户信息就是系统的资源,要访问资源需要通过URL,所以我们在controller中定义的每个http的接口就是访问资源的接口。

下边在controller中配置/r/r1需要p1权限,/r/r2需要p2权限。

hasAuthority(‘p1’)表示拥有p1权限方可访问。

代码如下:
在这里插入图片描述

@RestController
public class LoginController {
    ....
    @RequestMapping("/r/r1")
    @PreAuthorize("hasAuthority('p1')")//拥有p1权限方可访问
    public String r1(){
      return "访问r1资源";
    }
    
    @RequestMapping("/r/r2")
    @PreAuthorize("hasAuthority('p2')")//拥有p2权限方可访问
    public String r2(){
      return "访问r2资源";
    }
    ...

现在重启工程。

当访问以/r/开头的url时会判断用户是否认证,如果没有认证则跳转到登录页面,如果已经认证则判断用户是否具有该URL的访问权限,如果具有该URL的访问权限则继续,否则拒绝访问。

例如:

访问/r/r1,使用zhangsan登录可以正常访问,因为在/r/r1的方法上指定了权限p1,zhangsan用户拥有权限p1,所以可以正常访问。
在这里插入图片描述

访问/r/r1,使用lisi登录则拒绝访问,由于lisi用户不具有权限p1需要拒绝访问
在这里插入图片描述

注意:如果访问上不加@PreAuthorize,此方法没有授权控制。

整理授权的过程见下图所示:

在这里插入图片描述

2.2.4 工作原理

通过测试认证和授权两个功能,我们了解了Spring Security的基本使用方法,下边了解它的工作流程。

Spring Security所解决的问题就是安全访问控制,而安全访问控制功能其实就是对所有进入系统的请求进行拦截,校验每个请求是否能够访问它所期望的资源。根据前边知识的学习,可以通过Filter或AOP等技术来实现,Spring Security对Web资源的保护是靠Filter实现的,所以从这个Filter来入手,逐步深入Spring Security原理。

当初始化Spring Security时,会创建一个名为SpringSecurityFilterChain的Servlet过滤器,类型为 org.springframework.security.web.FilterChainProxy,它实现了javax.servlet.Filter,因此外部的请求会经过此类,下图是Spring Security过虑器链结构图:
在这里插入图片描述

FilterChainProxy是一个代理,真正起作用的是FilterChainProxy中SecurityFilterChain所包含的各个Filter,同时这些Filter作为Bean被Spring管理,它们是Spring Security核心,各有各的职责,但他们并不直接处理用户的认证,也不直接处理用户的授权,而是把它们交给了认证管理器(AuthenticationManager)和决策管理器(AccessDecisionManager)进行处理。

spring Security功能的实现主要是由一系列过滤器链相互配合完成。
在这里插入图片描述

下面介绍过滤器链中主要的几个过滤器及其作用:

  • SecurityContextPersistenceFilter 这个Filter是整个拦截过程的入口和出口(也就是第一个和最后一个拦截器),会在请求开始时从配置好的 SecurityContextRepository 中获取 SecurityContext,然后把它设置给 SecurityContextHolder。在请求完成后将 SecurityContextHolder 持有的 SecurityContext 再保存到配置好的 SecurityContextRepository,同时清除 securityContextHolder 所持有的 SecurityContext;
  • UsernamePasswordAuthenticationFilter 用于处理来自表单提交的认证。该表单必须提供对应的用户名和密码,其内部还有登录成功或失败后进行处理的 AuthenticationSuccessHandler 和 AuthenticationFailureHandler,这些都可以根据需求做相关改变;
  • FilterSecurityInterceptor 是用于保护web资源的,使用AccessDecisionManager对当前用户进行授权访问,前面已经详细介绍过了;
  • ExceptionTranslationFilter能够捕获来自 FilterChain 所有的异常,并进行处理。但是它只会处理两类异常:AuthenticationException 和 AccessDeniedException,其它的异常它会继续抛出。

Spring Security的执行流程如下:
在这里插入图片描述

  1. 用户提交用户名、密码被SecurityFilterChain中的UsernamePasswordAuthenticationFilter过滤器获取到,封装为请求Authentication,通常情况下是UsernamePasswordAuthenticationToken这个实现类。
  2. 然后过滤器将Authentication提交至认证管理器(AuthenticationManager)进行认证
  3. 认证成功后,AuthenticationManager身份管理器返回一个被填充满了信息的(包括上面提到的权限信息,身份信息,细节信息,但密码通常会被移除)Authentication实例。
  4. SecurityContextHolder安全上下文容器将第3步填充了信息的Authentication,通过SecurityContextHolder.getContext().setAuthentication(…)方法,设置到其中。
  5. 可以看出AuthenticationManager接口(认证管理器)是认证相关的核心接口,也是发起认证的出发点,它的实现类为ProviderManager。而Spring Security支持多种认证方式,因此ProviderManager维护着一个List<AuthenticationProvider>列表,存放多种认证方式,最终实际的认证工作是由AuthenticationProvider完成的。咱们知道web表单的对应的AuthenticationProvider实现类为DaoAuthenticationProvider,它的内部又维护着一个UserDetailsService负责UserDetails的获取。最终AuthenticationProvider将UserDetails填充至Authentication。

2.3 什么是OAuth2

2.3.1 OAuth2认证流程

在前边我们提到微信扫码认证,这是一种第三方认证的方式,这种认证方式是基于OAuth2协议实现,

OAUTH协议为用户资源的授权提供了一个安全的、开放而又简易的标准。同时,任何第三方都可以使用OAUTH认证服务,任何服务提供商都可以实现自身的OAUTH认证服务,因而OAUTH是开放的。业界提供了OAUTH的多种实现如PHP、JavaScript,Java,Ruby等各种语言开发包,大大节约了程序员的时间,因而OAUTH是简易的。互联网很多服务如Open API,很多大公司如Google,Yahoo,Microsoft等都提供了OAUTH认证服务,这些都足以说明OAUTH标准逐渐成为开放资源授权的标准。

Oauth协议目前发展到2.0版本,1.0版本过于复杂,2.0版本已得到广泛应用。

参考:https://baike.baidu.com/item/oAuth/7153134?fr=aladdin

Oauth协议:https://tools.ietf.org/html/rfc6749

下边分析一个Oauth2认证的例子,黑马程序员网站使用微信认证扫码登录的过程:
在这里插入图片描述

具体流程如下:

1、用户点击微信扫码
用户进入黑马程序的登录页面,点击微信的图标开打微信扫码界面。
在这里插入图片描述
在这里插入图片描述

微信扫码的目的是通过微信认证登录黑马程序员官网,黑马程序员网站需要从微信获取当前用户的身份信息才会让当前用户在黑马网站登录成功。

现在搞清楚几个概念:

  • 资源:用户信息,在微信中存储。
  • 资源拥有者:用户是用户信息资源的拥有者。
  • 认证服务:微信负责认证当前用户的身份,负责为客户端颁发令牌。
  • 客户端:客户端会携带令牌请求微信获取用户信息,黑马程序员网站即客户端,黑马网站需要在浏览器打开。

2、用户授权黑马网站访问用户信息

资源拥有者扫描二维码表示资源拥有者请求微信进行认证,微信认证通过向用户手机返回授权页面,如下图:
在这里插入图片描述

询问用户是否授权黑马程序员访问自己在微信的用户信息,用户点击“确认登录”表示同意授权,微信认证服务器会颁发一个授权码给黑马程序员的网站。

只有资源拥有者同意微信才允许黑马网站访问资源。

3、黑马程序员的网站获取到授权码

4、携带授权码请求微信认证服务器申请令牌

此交互过程用户看不到。

5、微信认证服务器向黑马程序员的网站响应令牌

此交互过程用户看不到。

6、黑马程序员网站请求微信资源服务器获取资源即用户信息。

黑马程序员网站携带令牌请求访问微信服务器获取用户的基本信息。

7、资源服务器返回受保护资源即用户信息

8、黑马网站接收到用户信息,此时用户在黑马网站登录成功。

理解了微信扫码登录黑马网站的流程,接下来认识Oauth2.0的认证流程,如下:

引自Oauth2.0协议rfc6749 https://tools.ietf.org/html/rfc6749
在这里插入图片描述

Oauth2包括以下角色:

1、客户端

  • 本身不存储资源,需要通过资源拥有者的授权去请求资源服务器的资源,比如:手机客户端、浏览器等。
  • 上边示例中黑马网站即为客户端,它需要通过浏览器打开。

2、资源拥有者

  • 通常为用户,也可以是应用程序,即该资源的拥有者。
  • A表示 客户端请求资源拥有者授权。
  • B表示 资源拥有者授权客户端即黑马网站访问自己的用户信息。

3、授权服务器(也称认证服务器)

  • 认证服务器对资源拥有者进行认证,还会对客户端进行认证并颁发令牌。
  • C 客户端即黑马网站携带授权码请求认证。
  • D认证通过颁发令牌。

4、资源服务器

  • 存储资源的服务器。
  • E表示客户端即黑马网站携带令牌请求资源服务器获取资源。
  • F表示资源服务器校验令牌通过后提供受保护资源。

2.3.2 OAuth2在本项目的应用

Oauth2是一个标准的开放的授权协议,应用程序可以根据自己的要求去使用Oauth2,本项目使用Oauth2实现如下目标:

1、学成在线访问第三方系统的资源。

  • 本项目要接入微信扫码登录所以本项目要使用OAuth2协议访问微信中的用户信息。

2、外部系统访问学成在线的资源 。

  • 同样当第三方系统想要访问学成在线网站的资源也可以基于OAuth2协议。

3、学成在线前端(客户端) 访问学成在线微服务的资源。

  • 本项目是前后端分离架构,前端访问微服务资源也可以基于OAuth2协议进行认证。

2.3.3 OAuth2的授权模式

Spring Security支持OAuth2认证,OAuth2提供授权码模式、密码模式、简化模式、客户端模式等四种授权模式,前边举的微信扫码登录的例子就是基于授权码模式,这四种模式中授权码模式和密码模式应用较多,本节使用Spring Security演示授权码模式、密码模式,其余两种请自行查阅相关资料。

2.3.3.1 授权码模式

OAuth2的几个授权模式是根据不同的应用场景以不同的方式去获取令牌,最终目的是要获取认证服务颁发的令牌,最终通过令牌去获取资源。

授权码模式简单理解是使用授权码去获取令牌,要想获取令牌先要获取授权码,授权码的获取需要资源拥有者亲自授权同意才可以获取。

下图是授权码模式的交互图:
在这里插入图片描述

还以黑马网站微信扫码登录为例进行说明:

1、用户打开浏览器。

2、通过浏览器访问客户端即黑马网站。

3、用户通过浏览器向认证服务请求授权,请求授权时会携带客户端的URL,此URL为下发授权码的重定向地址。

4、认证服务向资源拥有者返回授权页面。

5、资源拥有者亲自授权同意。

6、通过浏览器向认证服务发送授权同意。

7、认证服务向客户端地址重定向并携带授权码。

8、客户端即黑马网站收到授权码。

9、客户端携带授权码向认证服务申请令牌。

10、认证服务向客户端颁发令牌。

2.3.3.2 授权码模式测试

要想测试授权模式首先要配置授权服务器即上图中的认证服务器,需要配置授权服务及令牌策略。

1、从课程资料中拷贝 AuthorizationServer.java、TokenConfig.java到认证服务的config包下。
在这里插入图片描述

说明“:AuthorizationServer用 @EnableAuthorizationServer 注解标识并继承AuthorizationServerConfigurerAdapter来配置OAuth2.0 授权服务器。

package com.xuecheng.auth.config;
*/
 @Configuration
 @EnableAuthorizationServer
 public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {
 ...
 

AuthorizationServerConfigurerAdapter要求配置以下几个类:

public class AuthorizationServerConfigurerAdapter implements AuthorizationServerConfigurer {
    public AuthorizationServerConfigurerAdapter() {}
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {}
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {}
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {}
}

1)ClientDetailsServiceConfigurer:用来配置客户端详情服务(ClientDetailsService),
随便一个客户端都可以随便接入到它的认证服务吗?答案是否定的,服务提供商会给批准接入的客户端一个身份,用于接入时的凭据,有客户端标识和客户端秘钥,在这里配置批准接入的客户端的详细信息。

2)AuthorizationServerEndpointsConfigurer:用来配置令牌(token)的访问端点和令牌服务(token services)。

3)AuthorizationServerSecurityConfigurer:用来配置令牌端点的安全约束.

2、TokenConfig为令牌策略配置类

暂时先使用InMemoryTokenStore在内存存储令牌,令牌的有效期等信息配置如下:

//令牌管理服务
@Bean(name="authorizationServerTokenServicesCustom")
public AuthorizationServerTokenServices tokenService() {
    DefaultTokenServices service=new DefaultTokenServices();
    service.setSupportRefreshToken(true);//支持刷新令牌
    service.setTokenStore(tokenStore);//令牌存储策略
    service.setAccessTokenValiditySeconds(7200); // 令牌默认有效期2小时
    service.setRefreshTokenValiditySeconds(259200); // 刷新令牌默认有效期3天
    return service;
}

3、配置认证管理bean
在这里插入图片描述

@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

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

重启认证服务

1、get请求获取授权码
地址: http://localhost:63070/auth/oauth/authorize?client_id=XcWebApp&response_type=code&scope=all&redirect_uri=http://www.51xuecheng.cn

参数列表如下:

  • client_id:客户端准入标识。
  • response_type:授权码模式固定为code。
  • scope:客户端权限。
  • redirect_uri:跳转uri,当授权码申请成功后会跳转到此地址,并在后边带上code参数(授权码)。

输入账号zhangsan、密码123登录成功,输入http://localhost:63070/auth/oauth/authorize?client_id=XcWebApp&response_type=code&scope=all&redirect_uri=http://www.51xuecheng.cn

显示授权页面

在这里插入图片描述

授权“XcWebApp”访问自己的受保护资源?

选择同意。

2、请求成功,重定向至http://www.51xuecheng.cn/?code=授权码,比如:http://www.51xuecheng.cn/?code=Wqjb5H

在这里插入图片描述

3、使用httpclient工具post申请令牌:携带授权码申请令牌

  • /oauth/token?client_id=XcWebApp&client_secret=XcWebApp&grant_type=authorization_code
    &code=授权码&redirect_uri=http://www.51xuecheng.cn/

参数列表如下

  • client_id:客户端准入标识。
  • client_secret:客户端秘钥。
  • grant_type:授权类型,填写authorization_code,表示授权码模式
  • code:授权码,就是刚刚获取的授权码,注意:授权码只使用一次就无效了,需要重新申请。
  • redirect_uri:申请授权码时的跳转url,一定和申请授权码时用的redirect_uri一致。

httpclient脚本如下:

### 授权码模式
### 第一步申请授权码(浏览器请求)/oauth/authorize?client_id=c1&response_type=code&scope=all&redirect_uri=http://www.51xuecheng.cn(这一步使用浏览器做过了)
### 第二步申请令牌
POST {{auth_host}}/auth/oauth/token?client_id=XcWebApp&client_secret=XcWebApp&grant_type=authorization_code&code=自己的授权码&redirect_uri=http://www.51xuecheng.cn

申请令牌成功如下所示:
在这里插入图片描述

{
  "access_token": "368b1ee7-a9ee-4e9a-aae6-0fcab243aad2",
  "token_type": "bearer",
  "refresh_token": "3d56e139-0ee6-4ace-8cbe-1311dfaa991f",
  "expires_in": 7199,
  "scope": "all"
}

说明:

1、access_token,访问令牌,用于访问资源使用。

2、token_type,bearer是在RFC6750中定义的一种token类型,在携带令牌访问资源时需要在head中加入bearer 空格 令牌内容

3、refresh_token,当令牌快过期时使用刷新令牌可以再次生成令牌。

4、expires_in:过期时间(秒)

5、scope,令牌的权限范围,服务端可以根据令牌的权限范围去对令牌授权。

2.3.3.3 密码模式

密码模式相对授权码模式简单,授权码模式需要借助浏览器供用户亲自授权,密码模式不用借助浏览器,如下图:
在这里插入图片描述

1、资源拥有者提供账号和密码

2、客户端向认证服务申请令牌,请求中携带账号和密码

3、认证服务校验账号和密码正确颁发令牌。

开始测试:

1、POST请求获取令牌

  • /oauth/token?client_id=XcWebApp&client_secret=XcWebApp&grant_type=password&username=shangsan&password=123

参数列表如下:

  • client_id:客户端准入标识。
  • client_secret:客户端秘钥。
  • grant_type:授权类型,填写password表示密码模式
  • username:资源拥有者用户名。
  • password:资源拥有者密码。

2、授权服务器将令牌(access_token)发送给client

使用httpclient进行测试

### 密码模式
POST {{auth_host}}/auth/oauth/token?client_id=XcWebApp&client_secret=XcWebApp&grant_type=password&username=zhangsan&password=123

返回示例:
在这里插入图片描述

{
  "access_token": "368b1ee7-a9ee-4e9a-aae6-0fcab243aad2",
  "token_type": "bearer",
  "refresh_token": "3d56e139-0ee6-4ace-8cbe-1311dfaa991f",
  "expires_in": 6806,
  "scope": "all"
}

这种模式十分简单,但是却意味着直接将用户敏感信息泄漏给了client,因此这就说明这种模式只能用于client是我们自己开发的情况下。

2.3.3.4 本项目的应用方式

通过演示授权码模式和密码模式,授权码模式适合客户端和认证服务非同一个系统的情况,所以本项目使用授权码模式完成微信扫码认证。本项目采用密码模式作为前端请求微服务的认证方式。

2.4 JWT

2.4.1 普通令牌的问题

客户端申请到令牌,接下来客户端携带令牌去访问资源,到资源服务器将会校验令牌的合法性。

资源服务器如何校验令牌的合法性?

我们以OAuth2的密码模式为例进行说明:
在这里插入图片描述

从第4步开始说明:

1、客户端携带令牌访问资源服务获取资源。

2、资源服务远程请求认证服务校验令牌的合法性

3、如果令牌合法资源服务向客户端返回资源。

这里存在一个问题:

就是校验令牌需要远程请求认证服务,客户端的每次访问都会远程校验,执行性能低。

如果能够让资源服务自己校验令牌的合法性将省去远程请求认证服务的成本,提高了性能。如下图:
在这里插入图片描述

如何解决上边的问题,实现资源服务自行校验令牌。

令牌采用JWT格式即可解决上边的问题,用户认证通过后会得到一个JWT令牌,JWT令牌中已经包括了用户相关的信息,客户端只需要携带JWT访问资源服务,资源服务根据事先约定的算法自行完成令牌校验,无需每次都请求认证服务完成授权。

2.4.2 什么是JWT

什么是JWT?

JSON Web Token(JWT)是一种使用JSON格式传递数据的网络令牌技术,它是一个开放的行业标准(RFC 7519),它定义了一种简洁的、自包含的协议格式,用于在通信双方传递json对象,传递的信息经过数字签名可以被验证和信任,它可以使用HMAC算法或使用RSA的公钥/私钥对来签名,防止内容篡改。官网:https://jwt.io/

使用JWT可以实现无状态认证,什么是无状态认证?

传统的基于session的方式是有状态认证,用户登录成功将用户的身份信息存储在服务端,这样加大了服务端的存储压力,并且这种方式不适合在分布式系统中应用。

如下图,当用户访问应用服务,每个应用服务都会去服务器查看session信息,如果session中没有该用户则说明用户没有登录,此时就会重新认证,而解决这个问题的方法是Session复制、Session黏贴。
在这里插入图片描述

如果是基于令牌技术在分布式系统中实现认证则服务端不用存储session,可以将用户身份信息存储在令牌中,用户认证通过后认证服务颁发令牌给用户,用户将令牌存储在客户端,去访问应用服务时携带令牌去访问,服务端从jwt解析出用户信息。这个过程就是无状态认证。
在这里插入图片描述

JWT令牌的优点:

1、jwt基于json,非常方便解析。

2、可以在令牌中自定义丰富的内容,易扩展。

3、通过非对称加密算法及数字签名技术,JWT防止篡改,安全性高。

4、资源服务使用JWT可不依赖认证服务即可完成授权。

缺点:

1、JWT令牌较长,占存储空间比较大。

下边是一个JWT令牌的示例:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzMSJdLCJ1c2VyX25hbWUi
OiJ6aGFuZ3NhbiIsInNjb3BlIjpbImFsbCJdLCJleHAiOjE2NjQyNTQ2NzIsImF1dGhvcml0a
WVzIjpbInAxIl0sImp0aSI6Ijg4OTEyYjJkLTVkMDUtNGMxNC1iYmMzLWZkZTk5NzdmZWJjNi
IsImNsaWVudF9pZCI6ImMxIn0.wkDBL7roLrvdBG2oGnXeoXq-zZRgE9IVV2nxd-ez_oA

JWT令牌由三部分组成,每部分中间使用点(.)分隔,比如:xxxxx.yyyyy.zzzzz

1.Header
头部包括令牌的类型(即JWT)及使用的哈希算法(如HMAC SHA256或RSA)

一个例子如下:

下边是Header部分的内容

   {
    "alg": "HS256",
    "typ": "JWT"
  }

将上边的内容使用Base64Url编码,得到一个字符串就是JWT令牌的第一部分。

2 . Payload
第二部分是负载,内容也是一个json对象,它是存放有效信息的地方,它可以存放jwt提供的信息字段,比如:iss(签发者),exp(过期时间戳), sub(面向的用户)等,也可自定义字段。

此部分不建议存放敏感信息,因为此部分可以解码还原原始内容。

最后将第二部分负载使用Base64Url编码,得到一个字符串就是JWT令牌的第二部分。

一个例子:

  {
    "sub": "1234567890",
    "name": "456",
    "admin": true
  }

3 . Signature

第三部分是签名,此部分用于防止jwt内容被篡改。

这个部分使用base64url将前两部分进行编码,编码后使用点(.)连接组成字符串,最后使用header中声明的签名算法进行签名。

一个例子:

  HMACSHA256(
    base64UrlEncode(header) + "." +
    base64UrlEncode(payload),
    secret)

base64UrlEncode(header):jwt令牌的第一部分。

base64UrlEncode(payload):jwt令牌的第二部分。

secret:签名所使用的密钥。

为什么JWT可以防止篡改?

第三部分使用签名算法对第一部分和第二部分的内容进行签名,常用的签名算法是 HS256,常见的还有md5,sha 等,签名算法需要使用密钥进行签名,密钥不对外公开,并且签名是不可逆的,如果第三方更改了内容那么服务器验证签名就会失败,要想保证验证签名正确必须保证内容、密钥与签名前一致。
在这里插入图片描述

从上图可以看出认证服务和资源服务使用相同的密钥,这叫对称加密,对称加密效率高,如果一旦密钥泄露可以伪造jwt令牌。

JWT还可以使用非对称加密,认证服务自己保留私钥,将公钥下发给受信任的客户端、资源服务,公钥和私钥是配对的,成对的公钥和私钥才可以正常加密和解密,非对称加密效率低但相比对称加密非对称加密更安全一些。

2.4.3 测试生成JWT令牌

在认证服务中配置jwt令牌服务,即可实现生成jwt格式的令牌。

说明:使用springsecurity默认生成的普通令牌,修改之前的TokenConfig配置类生成jwt令牌。

在这里插入图片描述

package com.xuecheng.auth.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;

import java.util.Arrays;

/**
 * @author Administrator
 * @version 1.0
 **/
@Configuration
public class TokenConfig {

    private String SIGNING_KEY = "mq123";//miyao

    @Autowired
    TokenStore tokenStore;

//    @Bean
//    public TokenStore tokenStore() {
//        //使用内存存储令牌(普通令牌)
//        return new InMemoryTokenStore();
//    }

    @Autowired
    private JwtAccessTokenConverter accessTokenConverter;

    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(accessTokenConverter());
    }

    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey(SIGNING_KEY);
        return converter;
    }

    //令牌管理服务
    @Bean(name="authorizationServerTokenServicesCustom")
    public AuthorizationServerTokenServices tokenService() {
        DefaultTokenServices service=new DefaultTokenServices();
        service.setSupportRefreshToken(true);//支持刷新令牌
        service.setTokenStore(tokenStore);//令牌存储策略

        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        tokenEnhancerChain.setTokenEnhancers(Arrays.asList(accessTokenConverter));
        service.setTokenEnhancer(tokenEnhancerChain);

        service.setAccessTokenValiditySeconds(7200); // 令牌默认有效期2小时
        service.setRefreshTokenValiditySeconds(259200); // 刷新令牌默认有效期3天
        return service;
    }
}

重启认证服务。

使用httpclient通过密码模式申请令牌

### 密码模式
POST {{auth_host}}/oauth/token?client_id=XcWebApp&client_secret=XcWebApp&grant_type=password&username=zhangsan&password=123

生成jwt的示例如下:
在这里插入图片描述

{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzMSJdLCJ1c2VyX25hbWUiOiJ6aGFuZ3NhbiIsInNjb3BlIjpbImFsbCJdLCJleHAiOjE2NjQzMzE2OTUsImF1dGhvcml0aWVzIjpbInAxIl0sImp0aSI6ImU5ZDNkMGZkLTI0Y2ItNDRjOC04YzEwLTI1NmIzNGY4ZGZjYyIsImNsaWVudF9pZCI6ImMxIn0.-9SKI-qUqKhKcs8Gb80Rascx-JxqsNZxxXoPo82d8SM",
  "token_type": "bearer",
  "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzMSJdLCJ1c2VyX25hbWUiOiJ6aGFuZ3NhbiIsInNjb3BlIjpbImFsbCJdLCJhdGkiOiJlOWQzZDBmZC0yNGNiLTQ0YzgtOGMxMC0yNTZiMzRmOGRmY2MiLCJleHAiOjE2NjQ1ODM2OTUsImF1dGhvcml0aWVzIjpbInAxIl0sImp0aSI6ImRjNTRjNTRkLTA0YTMtNDIzNS04MmY3LTFkOWZkMmFjM2VmNSIsImNsaWVudF9pZCI6ImMxIn0.Wsw1Jc-Kd_GFqEugzdfoSsMY6inC8OQsraA21WjWtT8",
  "expires_in": 7199,
  "scope": "all",
  "jti": "e9d3d0fd-24cb-44c8-8c10-256b34f8dfcc"
}

1、access_token,生成的jwt令牌,用于访问资源使用。

2、token_type,bearer是在RFC6750中定义的一种token类型,在携带jwt访问资源时需要在head中加入bearer jwt令牌内容

3、refresh_token,当jwt令牌快过期时使用刷新令牌可以再次生成jwt令牌。

4、expires_in:过期时间(秒)

5、scope,令牌的权限范围,服务端可以根据令牌的权限范围去对令牌授权。

6、jti:令牌的唯一标识。

我们可以通过check_token接口校验jwt令牌(springsecurity提供的)

###校验jwt令牌  
### POST {{auth_host}}/auth/oauth/check_token?token=刚刚生成的jwt令牌

POST {{auth_host}}/auth/oauth/check_token?
token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzMSJdLCJ1c2VyX2
5hbWUiOiJzdHUxIiwic2NvcGUiOlsiYWxsIl0sImV4cCI6MTY2NDM3MTc4MCwiYXV0aG9yaXR
pZXMiOlsicDEiXSwianRpIjoiZjBhM2NkZWItMzk5ZC00OGYwLTg4MDQtZWNhNjM4YWQ4ODU3
IiwiY2xpZW50X2lkIjoiYzEifQ.qy46CSCJsH3eXWTHgdcntZhzcSzfRQlBU0dxAjZcsUw

响应示例如下:

在这里插入图片描述

{
  "aud": [
    "res1"
  ],
  "user_name": "zhangsan",
  "scope": [
    "all"
  ],
  "active": true,
  "exp": 1664371780,
  "authorities": [
    "p1"
  ],
  "jti": "f0a3cdeb-399d-48f0-8804-eca638ad8857",
  "client_id": "c1"
}

2.4.4 测试资源服务校验令牌

拿到了jwt令牌下一步就要携带令牌去访问资源服务中的资源,本项目各个微服务就是资源服务,比如:内容管理服务,客户端申请到jwt令牌,携带jwt去内容管理服务查询课程信息,此时内容管理服务要对jwt进行校验,只有jwt合法才可以继续访问。如下图:
在这里插入图片描述

1、在内容管理服务的content-api工程中添加依赖

<!--认证相关-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>

2、在内容管理服务的content-api工程中添加TokenConfig
在这里插入图片描述
在这里插入图片描述

package com.xuecheng.content.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;

/**
 * @author Administrator
 * @version 1.0
 **/
@Configuration
public class TokenConfig {

    //jwt签名密钥,与认证服务保持一致
    private String SIGNING_KEY = "mq123";

    @Bean
    public TokenStore tokenStore() {
        //JWT令牌存储方案
        return new JwtTokenStore(accessTokenConverter());
    }

    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey(SIGNING_KEY);
        return converter;
    }

   /* @Bean
    public TokenStore tokenStore() {
        //使用内存存储令牌(普通令牌)
        return new InMemoryTokenStore();
    }*/
}

3、添加资源服务配置

package com.xuecheng.content.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.TokenStore;

/**
 * @author Administrator
 * @version 1.0
 **/
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class ResouceServerConfig extends ResourceServerConfigurerAdapter {


    //资源服务标识
    public static final String RESOURCE_ID = "xuecheng-plus";

    @Autowired
    TokenStore tokenStore;

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        resources.resourceId(RESOURCE_ID)//资源 id
                .tokenStore(tokenStore)
                .stateless(true);
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests()
                .antMatchers("/r/**","/course/**").authenticated()//所有/r/**的请求必须认证通过
                .anyRequest().permitAll()
        ;
    }

}

根据配置可知/course/**开头的接口需要认证通过。

重启内容管理服务

使用httpclient测试:

1、访问根据课程id查询课程接口

GET http://localhost:63040/content/course/2

返回:

在这里插入图片描述

{
  "error": "unauthorized",
  "error_description": "Full authentication is required to access this resource"
}

从返回信息可知当前没有认证。

下边携带JWT令牌访问接口:

1、申请jwt令牌

采用密码模式申请令牌。
在这里插入图片描述

2、携带jwt令牌访问资源服务地址

在这里插入图片描述

### 携带token访问资源服务
GET http://localhost:63040/content/course/2
Authorization: Bearer 令牌信息



GET http://localhost:63040/content/course/2
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzMSJdLCJ1c2VyX25hbWUiOiJ6aGFuZ3NhbiIsInNjb3BlIjpbImFsbCJdLCJleHAiOjE2NjQzMzM0OTgsImF1dGhvcml0aWVzIjpbInAxIl0sImp0aSI6IjhhM2M2OTk1LWU1ZGEtNDQ1Yy05ZDAyLTEwNDFlYzk3NTkwOSIsImNsaWVudF9pZCI6ImMxIn0.73eNDxTX5ifttGCjwc7xrd-Sbp_mCfcIerI3lGetZto

如果携带jwt令牌且jwt正确则正常访问资源服务的内容。

如果不正确则报令牌无效的错误:

{
  "error": "invalid_token",
  "error_description": "Cannot convert access token to JSON"
}

2.4.5 测试获取用户身份

jwt令牌中记录了用户身份信息,当客户端携带jwt访问资源服务,资源服务验签通过后将前两部分的内容还原即可取出用户的身份信息,并将用户身份信息放在了SecurityContextHolder上下文,SecurityContext与当前线程进行绑定,方便获取用户身份。

还以查询课程接口为例,进入查询课程接口的代码中,添加获取用户身份的代码

在这里插入图片描述

@ApiOperation("根据课程id查询课程基础信息")
@GetMapping("/course/{courseId}")
public CourseBaseInfoDto getCourseBaseById(@PathVariable("courseId") Long courseId){
    //取出当前用户身份
    Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
    System.out.println(principal);

    return courseBaseInfoService.getCourseBaseInfo(courseId);
}

测试时需要注意:

1、首先在资源服务配置中指定安全拦截机制 /course/开头的请求需要认证,即请求/course/{courseId}接口需要携带jwt令牌且签证通过。
在这里插入图片描述

2、认证服务生成jwt令牌将用户身份信息写入令牌,认证服务哪里获取用户身份。
目前还是将用户信息硬编码并暂放在内存中,如下:

@Bean
public UserDetailsService userDetailsService() {
    //这里配置用户信息,这里暂时使用这种方式将用户存储在内存中
    InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
    manager.createUser(User.withUsername("zhangsan").password("123").authorities("p1").build());
    manager.createUser(User.withUsername("lisi").password("456").authorities("p2").build());
    return manager;
}

3、我们在使用密码模式生成jwt令牌时用的是zhangsan的信息,所以jwt令牌中存储了zhangsan的信息,那么在资源服务中应该取出zhangsan的信息才对。

清楚了以上内容,下边重启内容管理服务,跟踪取到的用户身份是否正确。
在这里插入图片描述

2.5 网关认证

2.5.1 技术方案

到目前为止,测试通过了认证服务颁发jwt令牌,客户端携带jwt访问资源服务,资源服务对jwt的合法性进行验证。如下图:
在这里插入图片描述

仔细观察此图,遗漏了本项目架构中非常重要的组件:网关,加上网关并完善后如下图所示:
在这里插入图片描述

所有访问微服务的请求都要经过网关,在网关进行用户身份的认证可以将很多非法的请求拦截到微服务以外,这叫做网关认证。

下边需要明确网关的职责:

1、网站白名单维护

针对不用认证的URL全部放行。

2、校验jwt的合法性。

除了白名单剩下的就是需要认证的请求,网关需要验证jwt的合法性,jwt合法则说明用户身份合法,否则说明身份不合法则拒绝继续访问。

网关负责授权吗?

网关不负责授权,对请求的授权操作在各个微服务进行,因为微服务最清楚用户有哪些权限访问哪些接口。

2.5.2 实现网关认证

下边实现网关认证,实现以下职责:

1、网站白名单维护

针对不用认证的URL全部放行。

2、校验jwt的合法性。

除了白名单剩下的就是需要认证的请求,网关需要验证jwt的合法性,jwt合法则说明用户身份合法,否则说明身份不合法则拒绝继续访问。

1、在网关工程添加依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
</dependency>

2、拷贝课程资料下网关认证配置类到网关工程的config包下。
在这里插入图片描述

3、配置白名单文件security-whitelist.properties

内容如下(持续补充)

在这里插入图片描述

#/**=临时全部放行
/auth/**=认证地址
/content/open/**=内容管理公开访问接口
/media/open/**=媒资管理公开访问接口

重启网关工程,进行测试

1、申请令牌
在这里插入图片描述

2、通过网关访问资源服务

这里访问内容管理服务

### 通过网关访问资源服务
GET http://localhost:63010/content/course/2
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzMSJdLCJ1c2VyX25hbWUiOiJ6aGFuZ3NhbiIsInNjb3BlIjpbImFsbCJdLCJleHAiOjE2NjQzNjIzMTAsImF1dGhvcml0aWVzIjpbInAxIl0sImp0aSI6Ijc2OTkwMGNiLWM1ZjItNGRiNC1hZWJmLWY1MzgxZDQxZWMyZCIsImNsaWVudF9pZCI6ImMxIn0.lOITjUgYg2HCh5mDPK9EvJJqz-tIupKVfmP8yWJQIKs

在这里插入图片描述

当token正确时可以正常访问资源服务,token验证失败返回token无效:
在这里插入图片描述

{
  "errMessage": "认证令牌无效"
}

注意:网关鉴权功能调试通过后,由于目前还没有开发认证功能,前端请求网关的URL不在白名单中间时会“没有认证”的错误,暂时在白名单中添加 全部放行配置,待认证功能开发完成再屏蔽全部放行配置,

在这里插入图片描述

由于是在网关处进行令牌校验,所以在微服务处不再校验令牌的合法性,修改内容管理服务的ResouceServerConfig类,屏蔽authenticated()。
在这里插入图片描述

 @Override
 public void configure(HttpSecurity http) throws Exception {
  http.csrf().disable()
          .authorizeRequests()
//          .antMatchers("/r/**","/course/**").authenticated()//所有/r/**的请求必须认证通过
          .anyRequest().permitAll()
  ;
 }

3 用户认证

3.1 需求分析

至此我们了解了使用Spring Security进行认证授权的过程,本节实现用户认证功能。
目前各大网站的认证方式非常丰富:账号密码认证、手机验证码认证、扫码登录等。
本项目也要支持多种认证试。

3.2 连接用户中心数据库

3.2.1 连接数据库认证

基于的认证流程在研究Spring Security过程中已经测试通过,到目前为止用户认证流程如下:
在这里插入图片描述

认证所需要的用户信息存储在用户中心数据库,现在需要将认证服务连接数据库查询用户信息。
在这里插入图片描述

在研究Spring Security的过程中是将用户信息硬编码,如下:

在这里插入图片描述

@Bean
public UserDetailsService userDetailsService() {
    //这里配置用户信息,这里暂时使用这种方式将用户存储在内存中
    InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
    manager.createUser(User.withUsername("zhangsan").password("123").authorities("p1").build());
    manager.createUser(User.withUsername("lisi").password("456").authorities("p2").build());
    return manager;
}

我们要认证服务中连接用户中心数据库查询用户信息。

如何使用Spring Security连接数据库认证吗?

前边学习Spring Security工作原理时有一张执行流程图,如下图:
在这里插入图片描述

用户提交账号和密码由DaoAuthenticationProvider调用UserDetailsService的loadUserByUsername()方法获取UserDetails用户信息。

查询DaoAuthenticationProvider的源代码如下:
在这里插入图片描述

UserDetailsService是一个接口,如下:

package org.springframework.security.core.userdetails;

public interface UserDetailsService {
    UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}

UserDetails是用户信息接口

public interface UserDetails extends Serializable {
    Collection<? extends GrantedAuthority> getAuthorities();

    String getPassword();

    String getUsername();

    boolean isAccountNonExpired();

    boolean isAccountNonLocked();

    boolean isCredentialsNonExpired();

    boolean isEnabled();
}

我们只要实现UserDetailsService 接口查询数据库得到用户信息返回UserDetails 类型的用户信息即可,框架调用loadUserByUsername()方法拿到用户信息之后是如何执行的,见下图:
在这里插入图片描述

首先屏蔽原来定义的UserDetailsService。
在这里插入图片描述

    //配置用户信息服务
//    @Bean
//    public UserDetailsService userDetailsService() {
//        //这里配置用户信息,这里暂时使用这种方式将用户存储在内存中
//        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
//        manager.createUser(User.withUsername("zhangsan").password("123").authorities("p1").build());
//        manager.createUser(User.withUsername("lisi").password("456").authorities("p2").build());
//        return manager;
//    }

下边自定义UserDetailsService

在这里插入图片描述

package com.xuecheng.ucenter.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.xuecheng.ucenter.mapper.XcUserMapper;
import com.xuecheng.ucenter.model.po.XcUser;
import org.springframework.beans.factory.annotation.Autowired;
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;

/**
 * ClassName: UserServiceImpl
 * Package: com.xuecheng.ucenter.service.impl
 * Description:
 *
 * @Author xxx
 * @Create 2024/6/24 20:45
 * @Version 1.0
 */
@Service
public class UserServiceImpl implements UserDetailsService {

    @Autowired
    XcUserMapper xcUserMapper;

    /**
     * @description 根据账号查询用户信息
     * @param s  账号
     * @return org.springframework.security.core.userdetails.UserDetails
     * @author Mr.M
     * @date 2022/9/28 18:30
     */
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {

        //1.根据username去xc_user表中查询对应的用户信息
        XcUser user = xcUserMapper.selectOne(new LambdaQueryWrapper<XcUser>().eq(XcUser::getUsername, s));
        //2.查询到用户不存在,只需要返回null即可,spring security框架会抛出异常:用户不存在
        if(user==null){
            //返回空表示用户不存在
            return null;
        }
        //3.如果查到了用户拿到正确的密码,最终封装成一个UserDetails对象给spring security框架返回,由框架进行密码比对
        //3.1取出数据库存储的正确密码
        String password  =user.getPassword();
        //3.2用户权限,如果不加报Cannot pass a null GrantedAuthority collection  (暂时这里写一个假数据)
        String[] authorities= {"test"};
        //3.3创建UserDetails对象,权限信息待实现授权功能时再向UserDetail中加入
        UserDetails userDetails = User
                .withUsername(user.getUsername())
                .password(password)
                .authorities(authorities)
                .build();

        return userDetails;
    }


}

数据库中的密码加过密的,用户输入的密码是明文,我们需要修改密码格式器PasswordEncoder,原来使用的是NoOpPasswordEncoder,它是通过明文方式比较密码,现在我们修改为BCryptPasswordEncoder,它是将用户输入的密码编码为BCrypt格式与数据库中的密码进行比对。

如下:
在这里插入图片描述

    @Bean
    public PasswordEncoder passwordEncoder() {

//        return NoOpPasswordEncoder.getInstance();//密码为明文方式
        return new BCryptPasswordEncoder(); //密码为加密方式
    }

我们通过测试代码测试BCryptPasswordEncoder,如下:
在这里插入图片描述

    public static void main(String[] args) {
        String password = "111111"; //原始密码
        PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        for(int i=0;i<5;i++) {
            //每个计算出的Hash值都不一样
            String hashPass = passwordEncoder.encode(password);
            System.out.println(hashPass);
            //校验密码,参数1居输入的明文,参數2是正确密码加密后的串
            //虽然每次计算的密码Hash值不一样但是校验是通过的
            boolean f = passwordEncoder.matches(password, hashPass);
            System.out.println(f);
        }
    }

在这里插入图片描述

修改数据库中的密码为Bcrypt格式,并且记录明文密码,稍后申请令牌时需要。

由于修改密码编码方式还需要将客户端的密钥更改为Bcrypt格式.

在这里插入图片描述

 @Override
  public void configure(ClientDetailsServiceConfigurer clients)
          throws Exception {
        clients.inMemory()// 使用in-memory存储
                .withClient("XcWebApp")// client_id
//                .secret("secret")//客户端密钥
                .secret(new BCryptPasswordEncoder().encode("XcWebApp"))//客户端密钥
                .resourceIds("xuecheng-plus")//资源列表
                .authorizedGrantTypes("authorization_code", "password","client_credentials","implicit","refresh_token")// 该client允许的授权类型authorization_code,password,refresh_token,implicit,client_credentials
                .scopes("all")// 允许的授权范围
                .autoApprove(false)//false跳转到授权页面
                //客户端接收授权码的重定向地址
                .redirectUris("http://www.51xuecheng.cn")
   ;
  }

现在重启认证服务。

下边使用httpclient进行测试:
在这里插入图片描述

### 密码模式
POST {{auth_host}}/oauth/token?client_id=XcWebApp&client_secret=XcWebApp&grant_type=password&username=stu1&password=111111

输入正确的账号和密码(user表存的有的),申请令牌成功。
在这里插入图片描述

{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsieHVlY2hlbmctcGx1cyJdLCJ1c2VyX25hbWUiOiJ0MSIsInNjb3BlIjpbImFsbCJdLCJleHAiOjE3MTkyNDMwNzcsImF1dGhvcml0aWVzIjpbInRlc3QiXSwianRpIjoiZWU1OWRkNWYtODE1Yi00ZGEyLTkyZDktMTQxZGNkZWNjY2NiIiwiY2xpZW50X2lkIjoiWGNXZWJBcHAifQ.DgZ7ZrYmTPseSMDRyRLeoCOjpiAPt4G6ytGD1XQ2R2w",
  "token_type": "bearer",
  "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsieHVlY2hlbmctcGx1cyJdLCJ1c2VyX25hbWUiOiJ0MSIsInNjb3BlIjpbImFsbCJdLCJhdGkiOiJlZTU5ZGQ1Zi04MTViLTRkYTItOTJkOS0xNDFkY2RlY2NjY2IiLCJleHAiOjE3MTk0OTUwNzcsImF1dGhvcml0aWVzIjpbInRlc3QiXSwianRpIjoiNzZkMzlhOWItZDFhYS00YTg3LWEzYWYtNGRhNWRlNmViOTgzIiwiY2xpZW50X2lkIjoiWGNXZWJBcHAifQ.GoRlXHyqg-mUJrX8ppa2oIkm_YFKmZ16FapxQnlambM",
  "expires_in": 7199,
  "scope": "all",
  "jti": "ee59dd5f-815b-4da2-92d9-141dcdeccccb"
}

输入错误的密码,报错:
在这里插入图片描述

{
  "error": "invalid_grant",
  "error_description": "用户名或密码错误"
}

输入错误的账号,报错:
在这里插入图片描述

{
  "error": "unauthorized",
  "error_description": "UserDetailsService returned null, which is an interface contract violation"
}

3.2.3 扩展用户身份信息

用户表中存储了用户的账号、手机号、email,昵称、qq等信息,UserDetails接口只返回了username、密码等信息,如下:
在这里插入图片描述
在这里插入图片描述

public interface UserDetails extends Serializable {
    Collection<? extends GrantedAuthority> getAuthorities();

    String getPassword();

    String getUsername();

    boolean isAccountNonExpired();

    boolean isAccountNonLocked();

    boolean isCredentialsNonExpired();

    boolean isEnabled();
}

我们需要扩展用户身份的信息,在jwt令牌中存储用户的昵称、头像、qq等信息。

如何扩展Spring Security的用户身份信息呢?

在认证阶段DaoAuthenticationProvider会调用UserDetailService查询用户的信息,这里是可以获取到齐全的用户信息的。由于JWT令牌中用户身份信息来源于UserDetails,UserDetails中仅定义了username为用户的身份信息,这里有两个思路:

  • 第一是可以扩展UserDetails(继承这个接口自定义一些属性),使之包括更多的自定义属性。
  • 第二也可以扩展username的内容 ,比如存入json数据内容作为username的内容。
  • 相比较而言,方案二比较简单还不用破坏UserDetails的结构,我们采用方案二。

修改UserServiceImpl如下:
在这里插入图片描述

package com.xuecheng.ucenter.service.impl;

import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.xuecheng.ucenter.mapper.XcUserMapper;
import com.xuecheng.ucenter.model.po.XcUser;
import org.springframework.beans.factory.annotation.Autowired;
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;

/**
 * ClassName: UserServiceImpl
 * Package: com.xuecheng.ucenter.service.impl
 * Description:
 *
 * @Author xxx
 * @Create 2024/6/24 20:45
 * @Version 1.0
 */
@Service
public class UserServiceImpl implements UserDetailsService {

    @Autowired
    XcUserMapper xcUserMapper;

    /**
     * @description 根据账号查询用户信息
     * @param s  账号
     * @return org.springframework.security.core.userdetails.UserDetails
     * @author Mr.M
     * @date 2022/9/28 18:30
     */
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {

        //1.根据username去xc_user表中查询对应的用户信息
        XcUser user = xcUserMapper.selectOne(new LambdaQueryWrapper<XcUser>().eq(XcUser::getUsername, s));
        //2.查询到用户不存在,只需要返回null即可,spring security框架会抛出异常:用户不存在
        if(user==null){
            //返回空表示用户不存在
            return null;
        }
        //3.如果查到了用户拿到正确的密码,最终封装成一个UserDetails对象给spring security框架返回,由框架进行密码比对
        //3.1取出数据库存储的正确密码
        String password  =user.getPassword();
        //3.2用户权限,如果不加报Cannot pass a null GrantedAuthority collection  (暂时这里写一个假数据)
        String[] authorities= {"test"};

        //为了安全在令牌中不放密码
        user.setPassword(null);
        //将user对象转json
        String userString = JSON.toJSONString(user);

        //3.3创建UserDetails对象,权限信息待实现授权功能时再向UserDetail中加入
        UserDetails userDetails = User
                //由原来只存放账号变为存放整个user的信息,并且为了安全
                .withUsername(userString) //user.getUsername()----》userString
                .password(password)
                .authorities(authorities)
                .build();

        return userDetails;
    }


}

重启认证服务,重新生成令牌,生成成功。
在这里插入图片描述

我们可以使用check_token查询jwt的内容

###校验jwt令牌
POST {{auth_host}}/oauth/check_token?token=生成的令牌

响应示例如下,

在这里插入图片描述

{
  "aud": [
    "res1"
  ],
  "user_name": "{\"birthday\":\"2022-09-28T19:28:46\",\"createTime\":\"2022-09-28T08:32:03\",\"id\":\"50\",\"name\":\"学生1\",\"nickname\":\"大水牛\",\"password\":\"$2a$10$0pt7WlfTbnPDTcWtp/.2Mu5CTXvohnNQhR628qq4RoKSc0dGAdEgm\",\"sex\":\"1\",\"status\":\"1\",\"username\":\"stu1\",\"userpic\":\"http://file.51xuecheng.cn/dddf\",\"utype\":\"101001\"}",
  "scope": [
    "all"
  ],
  "active": true,
  "exp": 1664372184,
  "authorities": [
    "p1"
  ],
  "jti": "73da9f7b-bd8c-45ac-9add-46b711d11fb8",
  "client_id": "c1"
}

user_name存储了用户信息的json格式,在资源服务中就可以取出该json格式的内容转为用户对象去使用。

3.2.3 资源服务获取用户身份

下边编写一个工具类在各个微服务中去使用,获取当前登录用户的对象。

在content-api中定义此类:
在这里插入图片描述

package com.xuecheng.content.util;

import com.alibaba.fastjson.JSON;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.context.SecurityContextHolder;

import java.io.Serializable;
import java.time.LocalDateTime;

/**
 * @author Mr.M
 * @version 1.0
 * @description 获取当前用户身份工具类
 * @date 2022/10/18 18:02
 */
@Slf4j
public class SecurityUtil {

    public static XcUser getUser() {
        try {

            //拿到当前用户身份
            Object principalObj = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
            if (principalObj instanceof String) {
                //取出用户身份信息
                String principal = principalObj.toString();
                //将json转成对象
                XcUser user = JSON.parseObject(principal, XcUser.class);
                return user;
            }
        } catch (Exception e) {
            log.error("获取当前登录用户身份出错:{}", e.getMessage());
            e.printStackTrace();
        }

        return null;
    }


    /**
     * 为什么使用成员内部类?
     * 解释:我们现在想要的是拿到用户的信息,此时是json类型所以需要把它转化为java实体类对象,
     *      这个实体类是定义在另一个认证服务工程中,当前content工程不想也不能依赖这个模块,
     *      此时只想要在这里使用一次,就可以定义为成员内部类的方式。
     */
    @Data
    public static class XcUser implements Serializable {

        private static final long serialVersionUID = 1L;

        private String id;

        private String username;

        private String password;

        private String salt;

        private String name;
        private String nickname;
        private String wxUnionid;
        private String companyId;
        /**
         * 头像
         */
        private String userpic;

        private String utype;

        private LocalDateTime birthday;

        private String sex;

        private String email;

        private String cellphone;

        private String qq;

        /**
         * 用户状态
         */
        private String status;

        private LocalDateTime createTime;

        private LocalDateTime updateTime;


    }


}

下边在内容管理服务中测试此工具类,以查询课程信息接口为例:

在这里插入图片描述

    //修改:
    @ApiOperation("根据课程id查询课程基础信息")
    @GetMapping("/course/{courseId}")
    public CourseBaseInfoDto getCourseBaseById(@PathVariable Long courseId){

        //取出当前用户身份
//        Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
//        System.out.println(principal);
        //改为使用工具类方式
        SecurityUtil.XcUser user = SecurityUtil.getUser();
        System.out.println(user);

        return courseBaseInfoService.getCourseBaseInfo(courseId);
    }

重启内容管理服务:

1、启动认证服务、网关、内容管理服务

2、生成新的令牌
在这里插入图片描述

3、携带令牌访问内容管理服务的查询课程接口
在这里插入图片描述

在这里插入图片描述

3.3 支持认证方式多样

3.3.1 统一认证入口

目前各大网站的认证方式非常丰富:账号密码认证、手机验证码认证、扫码登录等。基于当前研究的Spring Security认证流程如何支持多样化的认证方式呢?

1、支持账号和密码认证

  • 采用OAuth2协议的密码模式即可实现。

2、支持手机号加验证码认证

  • 用户认证提交的是手机号和验证码,并不是账号和密码。

3、微信扫码认证

  • 基于OAuth2协议与微信交互,学成在线网站向微信服务器申请到一个令牌,然后携带令牌去微信查询用户信息,查询成功则用户在学成在线项目认证通过。

目前我们测试通过OAuth2的密码模式,用户认证会提交账号和密码,由DaoAuthenticationProvider调用UserDetailsService的loadUserByUsername()方法获取UserDetails用户信息。

在前边我们自定义了UserDetailsService接口实现类,通过loadUserByUsername()方法根据账号查询用户信息。

而不同的认证方式提交的数据不一样,比如:

  • 手机加验证码方式:会提交手机号和验证码。
  • 账号密码方式:会提交账号、密码、验证码。

我们可以在loadUserByUsername()方法上作文章,将用户原来提交的账号数据改为提交json数据,json数据可以扩展不同认证方式所提交的各种参数。

首先创建一个DTO类表示认证的参数:

在这里插入图片描述

package com.xuecheng.ucenter.model.dto;

import lombok.Data;

import java.util.HashMap;
import java.util.Map;

/**
 * @author Mr.M
 * @version 1.0
 * @description 认证用户请求参数
 * @date 2022/9/29 10:56
 */
@Data
public class AuthParamsDto {

    private String username; //用户名
    private String password; //域  用于扩展
    private String cellphone;//手机号
    private String checkcode;//验证码
    private String checkcodekey;//验证码key
    private String authType; // 认证的类型   password:用户名密码模式类型    sms:短信模式类型
    private Map<String, Object> payload = new HashMap<>();//附加数据,作为扩展,不同认证类型可拥有不同的附加数据。如认证类型为短信时包含smsKey : sms:3d21042d054548b08477142bbca95cfa; 所有情况下都包含clientId


}

此时loadUserByUsername()方法可以修改如下:

在这里插入图片描述

package com.xuecheng.ucenter.service.impl;

import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.xuecheng.ucenter.mapper.XcUserMapper;
import com.xuecheng.ucenter.model.dto.AuthParamsDto;
import com.xuecheng.ucenter.model.po.XcUser;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
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;


@Slf4j
@Service
public class UserServiceImpl implements UserDetailsService {

    @Autowired
    XcUserMapper xcUserMapper;

    /**
     * @description 根据账号查询用户信息
     * @param s  账号----->传入的请求认证的参数就是AuthParamsDto
     * @return org.springframework.security.core.userdetails.UserDetails
     * @author Mr.M
     * @date 2022/9/28 18:30
     */
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {

        AuthParamsDto authParamsDto = null;
        try {
            //将认证参数转为AuthParamsDto类型
            authParamsDto = JSON.parseObject(s, AuthParamsDto.class);
        } catch (Exception e) {
            log.info("认证请求不符合项目要求:{}",s);
            throw new RuntimeException("认证请求数据格式不对");
        }

        //账号
        String userName = authParamsDto.getUsername();
        //1.根据username去xc_user表中查询对应的用户信息
        XcUser user = xcUserMapper.selectOne(new LambdaQueryWrapper<XcUser>().eq(XcUser::getUsername, userName));
        //2.查询到用户不存在,只需要返回null即可,spring security框架会抛出异常:用户不存在
        if(user==null){
            //返回空表示用户不存在
            return null;
        }
        //3.如果查到了用户拿到正确的密码,最终封装成一个UserDetails对象给spring security框架返回,由框架进行密码比对
        //3.1取出数据库存储的正确密码
        String password  =user.getPassword();
        //3.2用户权限,如果不加报Cannot pass a null GrantedAuthority collection  (暂时这里写一个假数据)
        String[] authorities= {"test"};

        //为了安全在令牌中不放密码
        user.setPassword(null);
        //将user对象转json
        String userString = JSON.toJSONString(user);

        //3.3创建UserDetails对象,权限信息待实现授权功能时再向UserDetail中加入
        UserDetails userDetails = User
                //由原来只存放账号变为存放整个user的信息,并且为了安全
                .withUsername(userString) //user.getUsername()----》userString
                .password(password)
                .authorities(authorities)
                .build();

        return userDetails;
    }


}

原来的DaoAuthenticationProvider 会进行密码校验,现在重新定义DaoAuthenticationProviderCustom类,重写类的additionalAuthenticationChecks方法。

说明:

  • 不是所有的认证都要验证密码,比如手机验证码就不需要校验密码。
  • 源码中的类DaoAuthenticationProvider 中的additionalAuthenticationChecks方法会进行校验密码,所以我们要重写这个方法不是所有的认证都需要验证密码。
    在这里插入图片描述

在这里插入图片描述

package com.xuecheng.auth.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;

/**
 * @description 自定义DaoAuthenticationProvider:重写了DaoAuthenticationProvider的校验密码的方法,
 *                                   因为我们统一了认证入口,有一些认证方式不需要校验密码
 * @author Mr.M
 * @date 2022/9/29 10:31
 * @version 1.0
 */
@Slf4j
@Component
public class DaoAuthenticationProviderCustom extends DaoAuthenticationProvider {


    @Autowired
    public void setUserDetailsService(UserDetailsService userDetailsService) {
        //调用父类的方法把自己定义的userDetailsService注入进来
        //class UserServiceImpl implements UserDetailsService:我们定义的是他的子类UserServiceImpl
        super.setUserDetailsService(userDetailsService);
    }


    //屏蔽密码对比:里面没写就表示不进行密码校验,以后想要校验自己写规则
    protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {


    }

}

修改WebSecurityConfig类指定daoAuthenticationProviderCustom

在这里插入图片描述

@Autowired //注入DaoAuthenticationProviderCustom这个自定义校验密码的类
DaoAuthenticationProviderCustom daoAuthenticationProviderCustom;


@Override //告诉框架你重写了这个类
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.authenticationProvider(daoAuthenticationProviderCustom);
}

此时可以重启认证服务,测试申请令牌接口,传入的账号信息改为json数据,如下:
在这里插入图片描述

################扩展认证请求参数后######################

###密码模式,请求AuthParamsDto参数
POST {{auth_host}}/auth/oauth/token?client_id=XcWebApp&client_secret=XcWebApp&grant_type=password&username=json串

POST {{auth_host}}/auth/oauth/token?client_id=XcWebApp&client_secret=XcWebApp&grant_type=password&username={"username":"stu1","authType":"password","password":"111111"}

经过测试发现loadUserByUsername()方法可以正常接收到认证请求中的json数据。
在这里插入图片描述

此时:

  • 我们就做好了统一认证入口的第一步工作:统一请求参数。
  • 有了这些认证参数我们可以定义一个认证Service接口去进行各种方式的认证。

定义用户信息,为了扩展性让它继承XcUser

  • 说明:这里最好不要直接用XcUser类,理由在之前的文章也说过,万一我们需要扩展一些其他的用户信息,那么我们直接修改XcUser类是不现实的,因为XcUser类对应的是数据库中的表。所以即使我们要使用XcUser类作为返回类型,也最好是让一个其他的类继承XcUser
@Data
public class XcUserExt extends XcUser {
}

定义认证Service 接口
在这里插入图片描述

package com.xuecheng.ucenter.service;

import com.xuecheng.ucenter.model.dto.AuthParamsDto;
import com.xuecheng.ucenter.model.po.XcUser;

/**
 * @description 统一认证接口
 * @author Mr.M
 * @date 2022/9/29 12:10
 * @version 1.0
 */
public interface AuthService {

   /**
    * @description 认证方法
    * @param authParamsDto 认证参数
    * @return com.xuecheng.ucenter.model.po.XcUser 用户信息
    * @author Mr.M
    * @date 2022/9/29 12:11
   */
   XcUserExt execute(AuthParamsDto authParamsDto);

}

定义认证Service 接口实现类:不同的接口实现类对应不同的认证方式。

在这里插入图片描述

/**
 * ClassName: PasswordAuthServiceImpl
 * Package: com.xuecheng.ucenter.service.impl
 * Description: 账号名密码方式认证
 *
 * @Author xxx
 * @Create 2024/6/25 18:08
 * @Version 1.0
 */
@Service("password_authservice") //因为注入AuthService接口时有不同的实现类,为了区分所以给实现类取个名字
public class PasswordAuthServiceImpl implements AuthService {
    @Override
    public XcUserExt execute(AuthParamsDto authParamsDto) {
        return null;
    }
}

在这里插入图片描述

/**
 * ClassName: WxAuthServiceImpl
 * Package: com.xuecheng.ucenter.service.impl
 * Description: 微信扫码认证
 *
 * @Author xxx
 * @Create 2024/6/25 18:10
 * @Version 1.0
 */
@Service("wx_authservice") //因为注入AuthService接口时有不同的实现类,为了区分所以给实现类取个名字
public class WxAuthServiceImpl implements AuthService {
    @Override
    public XcUserExt execute(AuthParamsDto authParamsDto) {
        return null;
    }
}

到此我们基于Spring Security认证流程修改为如下:

在这里插入图片描述

3.3.2 实现账号密码认证

上节定义了AuthService认证接口,下边实现该接口实现账号密码认证
在这里插入图片描述

package com.xuecheng.ucenter.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.xuecheng.ucenter.mapper.XcUserMapper;
import com.xuecheng.ucenter.model.dto.AuthParamsDto;
import com.xuecheng.ucenter.model.dto.XcUserExt;
import com.xuecheng.ucenter.model.po.XcUser;
import com.xuecheng.ucenter.service.AuthService;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

/**
 * ClassName: PasswordAuthServiceImpl
 * Package: com.xuecheng.ucenter.service.impl
 * Description: 账号名密码方式认证
 *
 * @Author xxx
 * @Create 2024/6/25 18:08
 * @Version 1.0
 */
@Service("password_authservice") //因为注入AuthService接口时有不同的实现类,为了区分所以给实现类取个名字
public class PasswordAuthServiceImpl implements AuthService {


    @Autowired
    XcUserMapper xcUserMapper;

    @Autowired
    PasswordEncoder passwordEncoder;//注入密码编码对象

    /**
     *
     * @param authParamsDto 认证参数对象
     * @return
     */
    @Override
    public XcUserExt execute(AuthParamsDto authParamsDto) {
        //账号
        String userName = authParamsDto.getUsername();

        //1.todo:校验验证码

        //2.账号是否存在,根据username账号查询数据库
        XcUser user = xcUserMapper.selectOne(new LambdaQueryWrapper<XcUser>().eq(XcUser::getUsername, userName));
        //查询到用户不存在,只需要返回null即可,spring security框架会抛出异常:用户不存在
        if(user==null){
            //返回空表示用户不存在
            return null;
        }

        //3.校验密码是否正确
        //如果查到了用户拿到正确的密码
        String passwordDb = user.getPassword();
        //拿到用户输入的密码
        String passwordForm = authParamsDto.getPassword();
        //校验密码:原始密码,加密后的密码
        boolean matches = passwordEncoder.matches(passwordForm, passwordDb);
        if(!matches){
            throw new RuntimeException("账号或密码错误");
        }
        XcUserExt xcUserExt = new XcUserExt();
        BeanUtils.copyProperties(user,xcUserExt);
        return xcUserExt;


    }
}

修改UserServiceImpl类,根据认证方式使用不同的认证bean
在这里插入图片描述

package com.xuecheng.ucenter.service.impl;

import com.alibaba.fastjson.JSON;
import com.xuecheng.ucenter.mapper.XcUserMapper;
import com.xuecheng.ucenter.model.dto.AuthParamsDto;
import com.xuecheng.ucenter.model.dto.XcUserExt;
import com.xuecheng.ucenter.service.AuthService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
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;


@Slf4j
@Service
public class UserServiceImpl implements UserDetailsService {

    @Autowired
    XcUserMapper xcUserMapper;

    @Autowired
    ApplicationContext applicationContext; //注入spring容器

    /**
     * @description 根据账号查询用户信息
     * @param s  账号----->传入的请求认证的参数就是AuthParamsDto
     * @return org.springframework.security.core.userdetails.UserDetails
     * @author Mr.M
     * @date 2022/9/28 18:30
     */
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {

        AuthParamsDto authParamsDto = null;
        try {
            //将认证参数转为AuthParamsDto类型
            authParamsDto = JSON.parseObject(s, AuthParamsDto.class);
        } catch (Exception e) {
            log.info("认证请求不符合项目要求:{}",s);
            throw new RuntimeException("认证请求数据格式不对");
        }

        //认证类型,有password,wx...
        String authType = authParamsDto.getAuthType();
        //beanName就是 认证类型 + 后缀,例如 password + _authservice = password_authservice
        String beanName = authType+"_authservice";
        //根据认证类型从spring容器中取出指定的bean
        AuthService authService = applicationContext.getBean(beanName,AuthService.class);
        //调用对应的类型来完成认证:比如 调用账号名密码方式认证
        XcUserExt xcUserExt = authService.execute(authParamsDto);

        //封装xcUserExt用户信息为UserDetails
        UserDetails userPrincipal = getUserPrincipal(xcUserExt);
        return userPrincipal;

    }

    /**
     * @description 查询用户信息:如果查到了用户拿到正确的密码,最终封装成一个UserDetails对象给spring security框架返回,
     *                         由框架进行密码比对
     * @param user  用户id,主键
     * @return com.xuecheng.ucenter.model.po.XcUser 用户信息
     * @author Mr.M
     * @date 2022/9/29 12:19
     */
    public UserDetails getUserPrincipal(XcUserExt user){
        //取出数据库存储的正确密码
        String password = user.getPassword();
        //用户权限,如果不加报Cannot pass a null GrantedAuthority collection  (暂时这里写一个假数据)
        String[] authorities = {"test"};

        //为了安全在令牌中不放密码
        user.setPassword(null);
        //将user对象转json
        String userString = JSON.toJSONString(user);

        //创建UserDetails对象,权限信息待实现授权功能时再向UserDetail中加入
        UserDetails userDetails = User
                //由原来只存放账号变为存放整个user的信息,并且为了安全
                .withUsername(userString) //user.getUsername()----》userString
                .password(password)
                .authorities(authorities)
                .build();

        return userDetails;
    }

}

重启认证服务,测试申请令牌接口

1、测试账号和密码都正确的情况是否可以申请令牌成功。

###密码模式,请求AuthParamsDto参数
POST {{auth_host}}/auth/oauth/token?client_id=XcWebApp&client_secret=XcWebApp&grant_type=password&username={"username":"stu1","authType":"password","password":"111111"}

在这里插入图片描述

2、测试密码错误的情况。

3、测试账号不存在情况。

3.4 验证码服务

3.4.1 创建验证码服务工程

在认证时一般都需要输入验证码,验证码有什么用?

验证码可以防止恶性攻击,比如:XSS跨站脚本攻击、CSRF跨站请求伪造攻击,一些比较复杂的图形验证码可以有效的防止恶性攻击。

为了保护系统的安全在一些比较重要的操作都需要验证码。
在这里插入图片描述

验证码的类型也有很多:图片、语音、手机短信验证码等。

本项目创建单独的验证码服务为各业务提供验证码的生成、校验等服务。

拷贝课程资料目录xuecheng-plus-checkcode验证码服务工程到自己的工程目录。
在这里插入图片描述

定义nacos配置文件
在这里插入图片描述

server:
  servlet:
    context-path: /checkcode
  port: 63075

注意修改bootstrap.yml中的命名空间为自己定义的命名空间。

在这里插入图片描述

配置redis-dev.yaml:生成的验证码存储在redis中
在这里插入图片描述

内容如下:

spring: 
  redis:
    host: 192.168.101.65
    port: 6379
    password: redis
    database: 0
    lettuce:
      pool:
        max-active: 20
        max-idle: 10
        min-idle: 0
    timeout: 10000
    #redisson:
      #配置文件目录
      #config: classpath:singleServerConfig.yaml

3.4.2 验证码接口测试

验证码服务对外提供的接口有:
1、生成验证码
2、校验验证码。

如下:
在这里插入图片描述

验证码服务如何生成并校验验证码?

拿图片验证码举例:

1、先生成一个指定位数的验证码,根据需要可能是数字、数字字母组合或文字。

2、根据生成的验证码生成一个图片并返回给页面

3、给生成的验证码分配一个key,将key和验证码一同存入缓存。这个key和图片一同返回给页面
在这里插入图片描述

4、用户输入验证码,连同key一同提交至认证服务。

5、认证服务拿key和输入的验证码请求验证码服务去校验

6、验证码服务根据key从缓存取出正确的验证码和用户输入的验证码进行比对,如果相同则校验通过,否则不通过。
在这里插入图片描述

根据接口分析,验证码服务接口如下:

package com.xuecheng.checkcode.controller;

import com.xuecheng.base.model.RestResponse;
import com.xuecheng.checkcode.model.CheckCodeParamsDto;
import com.xuecheng.checkcode.model.CheckCodeResultDto;
import com.xuecheng.checkcode.service.CheckCodeService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import java.util.Map;

/**
 * @author Mr.M
 * @version 1.0
 * @description 验证码服务接口
 * @date 2022/9/29 18:39
 */
@Api(value = "验证码服务接口")
@RestController
public class CheckCodeController {


    @ApiOperation(value="生成验证信息", notes="生成验证信息")
    @PostMapping(value = "/pic")
    public CheckCodeResultDto generatePicCheckCode(CheckCodeParamsDto checkCodeParamsDto){
        
    }

    @ApiOperation(value="校验", notes="校验")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "name", value = "业务名称", required = true, dataType = "String", paramType="query"),
            @ApiImplicitParam(name = "key", value = "验证key", required = true, dataType = "String", paramType="query"),
            @ApiImplicitParam(name = "code", value = "验证码", required = true, dataType = "String", paramType="query")
    })
    @PostMapping(value = "/verify")
    public Boolean verify(String key, String code){
        
    }
}

1、生成验证码接口
在这里插入图片描述

### 申请验证码
POST {{checkcode_host}}/checkcode/pic
  • key:验证码
  • aliasing:验证码图片,已经转化为base64字符串类型。
  • 把这个aliasing地址放到浏览器上访问:
    在这里插入图片描述

2、校验验证码接口
根据生成验证码返回的key以及日志中输出正确的验证码去测试。

在这里插入图片描述

### 校验验证码
POST {{checkcode_host}}/checkcode/verify?key=上面生成的key=上面图片上的验证码

3.5 账号密码认证

3.5.1 需求分析

到目前为止账号和密码认证所需要的技术、组件都已开发完毕,下边实现账号密码认证,输出如下图:
在这里插入图片描述

执行流程如下:
在这里插入图片描述

3.5.2 账号密码认证开发

1、在认证服务定义远程调用验证码服务的接口

说明:在认证服务中调用验证码服务接口,所以需要远程调用。
在这里插入图片描述

/**
 * @description 搜索服务远程接口
 * @author Mr.M
 * @date 2022/9/20 20:29
 * @version 1.0
 */
//服务名,降级的逻辑
@FeignClient(value = "checkcode",fallbackFactory = CheckCodeClientFactory.class)
@RequestMapping("/checkcode")
public interface CheckCodeClient {

    @PostMapping(value = "/verify")
    public Boolean verify(@RequestParam("key") String key, @RequestParam("code") String code);

}

CheckCodeClientFactory:降级逻辑
在这里插入图片描述

@Slf4j
@Component
public class CheckCodeClientFactory implements FallbackFactory<CheckCodeClient> {
    @Override
    public CheckCodeClient create(Throwable throwable) {
        return new CheckCodeClient() {

            @Override
            public Boolean verify(String key, String code) {
                log.debug("调用验证码服务熔断异常:{}", throwable.getMessage());
                return null;
            }
        };
    }
}

启动类添加:
在这里插入图片描述

@EnableFeignClients(basePackages={"com.xuecheng.*.feignclient"})

配置文件引入feign-dev.yaml
在这里插入图片描述

- data-id: feign-${spring.profiles.active}.yaml
  group: xuecheng-plus-common
  refresh: true

2、完善PasswordAuthServiceImpl
在这里插入图片描述

package com.xuecheng.ucenter.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.xuecheng.ucenter.feignclient.CheckCodeClient;
import com.xuecheng.ucenter.mapper.XcUserMapper;
import com.xuecheng.ucenter.model.dto.AuthParamsDto;
import com.xuecheng.ucenter.model.dto.XcUserExt;
import com.xuecheng.ucenter.model.po.XcUser;
import com.xuecheng.ucenter.service.AuthService;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

/**
 * ClassName: PasswordAuthServiceImpl
 * Package: com.xuecheng.ucenter.service.impl
 * Description: 账号名密码方式认证
 *
 * @Author xxx
 * @Create 2024/6/25 18:08
 * @Version 1.0
 */
@Service("password_authservice") //因为注入AuthService接口时有不同的实现类,为了区分所以给实现类取个名字
public class PasswordAuthServiceImpl implements AuthService {


    @Autowired
    XcUserMapper xcUserMapper;

    @Autowired
    PasswordEncoder passwordEncoder;//注入密码编码对象

    @Autowired
    CheckCodeClient checkCodeClient;

    /**
     *
     * @param authParamsDto 认证参数对象
     * @return
     */
    @Override
    public XcUserExt execute(AuthParamsDto authParamsDto) {
        //账号
        String userName = authParamsDto.getUsername();
        //输入的验证码
        String checkcode = authParamsDto.getCheckcode();
        //验证码对应的key
        String checkcodekey = authParamsDto.getCheckcodekey();
        if(StringUtils.isEmpty(checkcode)||StringUtils.isEmpty(checkcode)){
            throw new RuntimeException("请输入验证码");
        }

        //1.远程调用验证码服务接口去校验验证码:因为验证码服务接口是在另一个验证码工程模块中:key  code
        Boolean verify = checkCodeClient.verify(checkcodekey, checkcode);
        if(verify == null || !verify){
            throw new RuntimeException("验证码输入错误");
        }


        //2.账号是否存在,根据username账号查询数据库
        XcUser user = xcUserMapper.selectOne(new LambdaQueryWrapper<XcUser>().eq(XcUser::getUsername, userName));
        //查询到用户不存在,只需要返回null即可,spring security框架会抛出异常:用户不存在
        if(user==null){
            //返回空表示用户不存在
            return null;
        }

        //3.校验密码是否正确
        //如果查到了用户拿到正确的密码
        String passwordDb = user.getPassword();
        //拿到用户输入的密码
        String passwordForm = authParamsDto.getPassword();
        //校验密码:原始密码,加密后的密码
        boolean matches = passwordEncoder.matches(passwordForm, passwordDb);
        if(!matches){
            throw new RuntimeException("账号或密码错误");
        }
        XcUserExt xcUserExt = new XcUserExt();
        BeanUtils.copyProperties(user,xcUserExt);
        return xcUserExt;


    }
}

小技巧:目前账号密码方式添加了验证码校验,为了后期获取令牌方便可以重新定义一个不需要验证码校验的认证类AuthService ,AuthService 中去掉验证码的校验,方便生成令牌。

3.5.3 账号密码认证测试

1、启动nginx、网关、验证码模块。

1、使用浏览器访问 http://www.51xuecheng.cn/sign.html
在这里插入图片描述

2、首先测试验证码,分别输入正确的验证码和错误的验证码进行测试
在这里插入图片描述
在这里插入图片描述

3、输入正确的账号密码和错误的账号密码进行测试

  • 登录成功将jwt令牌存储cookie.
    在这里插入图片描述

4、测试自动登录

  • 勾选自动登录cookie生成时间为30天,不勾选自动登录关闭浏览器窗口后自动删除cookie。

4 微信扫码登录

4.1 接入规范

说明:和第三方对接时的步骤

  • 找到官方的接口文档。
  • 一步步的阅读文档。
  • 在阅读的过程中清楚那些是你要的参数。

4.1.1 接入流程

  • 接口文档:https://developers.weixin.qq.com/doc/oplatform/Website_App/WeChat_Login/Wechat_Login.html

  • 微信OAuth2.0授权登录目前支持authorization_code模式,适用于拥有 server 端的应用授权。该模式整体流程为:

    • 第三方发起微信授权登录请求,微信用户允许授权第三方应用后,微信会拉起应用或重定向到第三方网站,并且带上授权临时票据 code 参数;
    • 通过 code 参数加上 AppID 和AppSecret等,通过 API 换取access_token;
    • 通过access_token进行接口调用,获取用户基本数据资源或帮助用户实现基本操作。
  • 获取access_token时序图:
    在这里插入图片描述

  • 第三方应用获取access_token令牌后即可请求微信获取用户的信息,成功获取到用户的信息表示用户在第三方应用认证成功。

4.1.2 第一步:请求获取授权码

第三方使用网站应用授权登录前请注意已获取相应网页授权作用域(scope=snsapi_login),则可以通过在 PC 端打开以下链接: https://open.weixin.qq.com/connect/qrconnect?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect 若提示“该链接无法访问”,请检查参数是否填写错误,如redirect_uri的域名与审核时填写的授权域名不一致或 scope 不为snsapi_login。

参数说明
在这里插入图片描述

返回说明

用户允许授权后,将会重定向到redirect_uri的网址上,并且带上 code 和state参数

redirect_uri?code=CODE&state=STATE

若用户禁止授权,则不会发生重定向。

请求示例

登录一号店网站应用 https://test.yhd.com/wechat/login.do 打开后,一号店会生成 state 参数,跳转到 https://open.weixin.qq.com/connect/qrconnect?appid=wxbdc5610cc59c1631&redirect_uri=https%3A%2F%2Fpassport.yhd.com%2Fwechat%2Fcallback.do&response_type=code&scope=snsapi_login&state=3d6be0a4035d839573b04816624a415e#wechat_redirect 微信用户使用微信扫描二维码并且确认登录后,PC端会跳转到 https://test.yhd.com/wechat/callback.do?code=CODE&state=3d6be0a40sssssxxxxx6624a415e

将微信登录二维码内嵌到自己页面

为了满足网站更定制化的需求,我们还提供了第二种获取 code 的方式,支持网站将微信登录二维码内嵌到自己页面中,用户使用微信扫码授权后通过 JS 将code返回给网站。 JS微信登录主要用途:网站希望用户在网站内就能完成登录,无需跳转到微信域下登录后再返回,提升微信登录的流畅性与成功率。 网站内嵌二维码微信登录 JS 实现办法:

步骤1:在页面中先引入如下 JS 文件(支持https):

http://res.wx.qq.com/connect/zh_CN/htmledition/js/wxLogin.js

步骤2:在需要使用微信登录的地方实例以下 JS 对象:

 var obj = new WxLogin({
	 self_redirect:true,
	 id:"login_container", 
	 appid: "", 
	 scope: "", 
	 redirect_uri: "",
	  state: "",
	 style: "",
	 href: ""
 });

参数说明
在这里插入图片描述
在这里插入图片描述

4.1.3 第二步:通过 code 获取access_token

通过 code 获取access_token

https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code

参数说明
在这里插入图片描述

返回说明

正确的返回:

{ 
"access_token":"ACCESS_TOKEN", 
"expires_in":7200, 
"refresh_token":"REFRESH_TOKEN",
"openid":"OPENID", 
"scope":"SCOPE",
"unionid": "o6_bmasdasdsad6_2sgVt7hMZOPfL"
}

参数说明

在这里插入图片描述

错误返回样例:

{"errcode":40029,"errmsg":"invalid code"}

…等等(查看官方文档)

4.1.4 第三步:通过access_token调用接口

获取access_token后,进行接口调用,有以下前提:

access_token有效且未超时;
微信用户已授权给第三方应用帐号相应接口作用域(scope)。

对于接口作用域(scope),能调用的接口有以下:

在这里插入图片描述

其中snsapi_base属于基础接口,若应用已拥有其它 scope 权限,则默认拥有snsapi_base的权限。使用snsapi_base可以让移动端网页授权绕过跳转授权登录页请求用户授权的动作,直接跳转第三方网页带上授权临时票据(code),但会使得用户已授权作用域(scope)仅为snsapi_base,从而导致无法获取到需要用户授权才允许获得的数据和基础功能。 接口调用方法可查阅《微信授权关系接口调用指南》

获取用户信息接口文档:

https://developers.weixin.qq.com/doc/oplatform/Website_App/WeChat_Login/Authorized_Interface_Calling_UnionID.html

接口地址

http请求方式: GET
https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID

如下:

在这里插入图片描述

响应:

{
"openid":"OPENID",
"nickname":"NICKNAME",
"sex":1,
"province":"PROVINCE",
"city":"CITY",
"country":"COUNTRY",
"headimgurl": "https://thirdwx.qlogo.cn/mmopen/g3MonUZtNHkdmzicIlibx6iaFqAc56vxLSUfpb6n5WKSYVY0ChQKkiaJSgQ1dZuTOgvLLrhJbERQQ4eMsv84eavHiaiceqxibJxCfHe/0",
"privilege":[
"PRIVILEGE1",
"PRIVILEGE2"
],
"unionid": " o6_bmasdasdsad6_2sgVt7hMZOPfL"

}

说明如下:

参数说明
openid普通用户的标识,对当前开发者帐号唯一
nickname普通用户昵称
sex普通用户性别,1为男性,2为女性
province普通用户个人资料填写的省份
city普通用户个人资料填写的城市
country国家,如中国为CN
headimgurl用户头像,最后一个数值代表正方形头像大小(有0、46、64、96、132数值可选,0代表640*640正方形头像),用户没有头像时该项为空
privilege用户特权信息,json数组,如微信沃卡用户为(chinaunicom)
unionid用户统一标识。针对一个微信开放平台帐号下的应用,同一用户的 unionid 是唯一的。

4.2 准备开发环境

4.2.1 添加应用

1、注册微信开放平台

https://open.weixin.qq.com/

2、添加应用

进入网站应用,添加应用

在这里插入图片描述

3、添加应用需要指定一个外网域名作为微信回调域名

  • 审核通过后,生成app密钥。

  • 最终获取appID和AppSecret

总结:

  • 需要在开发平台注册,上传营业执照,还需要一个公网域名,最终生成一个appID和秘钥。

解决:

  • 这里的AppId和回调地址是尚硅谷提供的,所以我们必须修改认证服务的端口为8160,不然不能用。
    • 因为在微信申请appid的时候就要求指定域名,如果redirect_uri参数和指定的域名不一样,那么微信会拒绝回调的。尚硅谷的老师申请的appid对应的域名就是localhost:8160,所以我们redirect_uri也只能填这个。
  • 修改nacos中的auth-service-dev.yaml,修改端口、appid、appsecret
    在这里插入图片描述
server:
  port: 8160
weixin:
  appid: wxed9954c01bb89b47
  secret: a7482517235173ddb4083788de60b90e
  • 前端的appid需要修改

  • 前端的redirect_uri为:http://localhost:8160/auth/wxLogin
    在这里插入图片描述
    在这里插入图片描述

    • 刷新页面,发现已经修改
      在这里插入图片描述
      在这里插入图片描述

4.2.2 内网穿透

我们的开发环境在局域网,微信回调指向一个公网域名。

  • 说明:申请授权码是微信重定向的方式返回授权码在公网环境,而自己的电脑在开发环境局域网。局域网访问公网就需要内网穿透工具了。

如何让微信回调请求至我们的开发计算机上呢?

可以使用内网穿透技术,什么是内网穿透?

内网穿透简单来说就是将内网外网通过隧道打通,让内网的数据让外网可以获取。比如常用的办公室软件等,一般在办公室或家里,通过拨号上网,这样办公软件只有在本地的局域网之内才能访问,那么问题来了,如果是手机上,或者公司外地的办公人员,如何访问到办公软件呢?这就需要内网穿透工具了。开启隧道之后,网穿透工具会分配一个专属域名/端口,办公软件就已经在公网上了,在外地的办公人员可以在任何地方愉快的访问办公软件了~~

在这里插入图片描述

1、在内网穿透服务器上开通隧道,配置外网域名,配置穿透内网的端口即本地电脑上的端口。
在这里插入图片描述

  • 这里我们配置认证服务端口,最终实现通过外网域名访问本地认证服务。
  • 如果服务上线,公网访问公网就不需要内网穿透工具了。

2、在本地电脑上安装内网穿透的工具,工具上配置内网穿透服务器隧道token。

  • 市面上做内网穿透的商家很多,需要时可以查阅资料了解下。

4.3 接入微信登录

4.3.1 接入分析

根据OAuth2协议授权码流程,结合本项目自身特点,分析接入微信扫码登录的流程如下:
在这里插入图片描述

本项目认证服务需要做哪些事?

1、需要定义接口接收微信下发的授权码。

2、收到授权码调用微信接口申请令牌。

3、申请到令牌调用微信获取用户信息

4、获取用户信息成功将其写入本项目用户中心数据库。

5、最后重定向到浏览器自动登录。

4.3.2 定义接口接收授权码

参考接口规范中“请求获取授权码” 定义接收微信下发的授权码接口,
定义WxLoginController类,如下:
在这里插入图片描述

@Slf4j
@Controller
public class WxLoginController {

    /**
     *
     * @param code  授权码
     * @param state  用于保持请求和回调的状态
     * @return
     * @throws IOException
     */
    @RequestMapping("/wxLogin")
    public String wxLogin(String code, String state) throws IOException {
        log.debug("微信扫码回调,code:{},state:{}",code,state);
        //todo:远程调用(这里使用restTemplate)微信申请令牌,拿到令牌查询用户信息,将用户信息写入本项目数据库
        XcUser xcUser = new XcUser();
        //暂时硬编写,目的是调试环境(从数据库里找一个已经存在的用户,这个用户当成从微信过来的用户)
        xcUser.setUsername("t1");
        if(xcUser==null){
            return "redirect:http://www.51xuecheng.cn/error.html";
        }
        String username = xcUser.getUsername();
        //重定向登录页面自动登录    认证的类型:wx
        return "redirect:http://www.51xuecheng.cn/sign.html?username="+username+"&authType=wx";
    }
}

定义微信认证的service
在这里插入图片描述

package com.xuecheng.ucenter.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.xuecheng.ucenter.mapper.XcUserMapper;
import com.xuecheng.ucenter.model.dto.AuthParamsDto;
import com.xuecheng.ucenter.model.dto.XcUserExt;
import com.xuecheng.ucenter.model.po.XcUser;
import com.xuecheng.ucenter.service.AuthService;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * ClassName: WxAuthServiceImpl
 * Package: com.xuecheng.ucenter.service.impl
 * Description: 微信扫码认证
 *
 * @Author xxx
 * @Create 2024/6/25 18:10
 * @Version 1.0
 */
@Service("wx_authservice") //因为注入AuthService接口时有不同的实现类,为了区分所以给实现类取个名字
public class WxAuthServiceImpl implements AuthService {
    @Autowired
    XcUserMapper xcUserMapper;

    @Override
    public XcUserExt execute(AuthParamsDto authParamsDto) {
        //账号
        String username = authParamsDto.getUsername();
        //查询数据库
        XcUser user = xcUserMapper.selectOne(new LambdaQueryWrapper<XcUser>().eq(XcUser::getUsername, username));
        if(user==null){
            //返回空表示用户不存在
            throw new RuntimeException("账号不存在");
        }
        XcUserExt xcUserExt = new XcUserExt();
        BeanUtils.copyProperties(user,xcUserExt);
        return xcUserExt;
    }
}

4.3.3 接口环境测试

接口定义好下边进行测试下,主要目的是测试接口调度的环境。

1、启动内网穿透工具(这里不需要启动,不知道为什么)

2、在/wxLogin接口中打断点

3、打开前端微信扫码页面
在这里插入图片描述

点击微信图标打开二维码
在这里插入图片描述

用户扫码,确认授权

此时正常进入 /wxLogin 方法,最后跳转到http://www.51xuecheng.cn/sign.html?username=t1&authType=wx。
在这里插入图片描述

4.3.4 申请令牌

接下来请求微信申请令牌。

1、使用restTemplate请求微信,配置RestTemplate bean

说明:

  • 远程调用在微服务之间用的是fegin,在和第三方远程调用用的是restTemplate。

在启动类配置restTemplate
在这里插入图片描述

@Bean
RestTemplate restTemplate(){
    RestTemplate restTemplate = new RestTemplate(new OkHttp3ClientHttpRequestFactory());
    return  restTemplate;
}

定义与微信认证的service接口:
在这里插入图片描述

/**
 * @author Mr.M
 * @version 1.0
 * @description 微信认证接口
 * @date 2023/2/21 22:15
 */
public interface WxAuthService {

    public XcUser wxAuth(String code);

}

继续在WxAuthServiceImpl类写WxAuthService 接口的实现,在其中定义申请令牌的私有方法并由wxAuth方法去调用:
在这里插入图片描述


    @Value("${weixin.appid}") //在微信平台注册后会自动生成,在nacos中已经配好了,所以引入即可。
    String appid;
    @Value("${weixin.secret}")
    String secret;

    @Autowired
    RestTemplate restTemplate;

    @Override
    public XcUser wxAuth(String code) {

        //1.申请令牌
        Map<String, String> access_token_map = getAccess_token(code);
        System.out.println(access_token_map);
        if(access_token_map==null){
            return null;
        }

        //2.携带令牌查询用户信息

        //3.保存用户信息到数据库
        return null;
    }



    /**
     * 携带授权码申请令牌
     * https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code
     *
     * 响应示例
     {
     "access_token":"ACCESS_TOKEN",
     "expires_in":7200,
     "refresh_token":"REFRESH_TOKEN",
     "openid":"OPENID",
     "scope":"SCOPE",
     "unionid": "o6_bmasdasdsad6_2sgVt7hMZOPfL"
     }
     */
    private Map<String,String> getAccess_token(String code) {

        //微信平台提供的url根据授权码获取令牌:appid唯一标识   密钥AppSecret    code授权码    grant_type(填authorization_code 固定值)
        //%s:占位符
        String wxUrl_template = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code";
        //最终的请求路径:类似于日志占位符,把参数的内容替换掉对应的占位符。
        String wxUrl = String.format(wxUrl_template, appid, secret, code);

        log.info("调用微信接口申请access_token, url:{}", wxUrl);

        //远程调用此url:url   请求方式   请求的内容(现在参数是设置在地址后面,所以不用挂载内容了)  响应的类型(字符串)
        ResponseEntity<String> exchange = restTemplate.exchange(wxUrl, HttpMethod.POST, null, String.class);

        //获取响应的结果
        String result = exchange.getBody();
        log.info("调用微信接口申请access_token: 返回值:{}", result);
        //将result转化为Map
        Map<String,String> resultMap = JSON.parseObject(result, Map.class);

        return resultMap;
    }
....

下边在controller中调用wxAuth接口:
在这里插入图片描述

@Slf4j
@Controller
public class WxLoginController {
    @Autowired
    WxAuthService wxAuthService;

    /**
     * @param code  授权码
     * @param state  用于保持请求和回调的状态
     * @return
     * @throws IOException
     */
    @RequestMapping("/wxLogin")
    public String wxLogin(String code, String state) throws IOException {
        log.debug("微信扫码回调,code:{},state:{}",code,state);
        //远程调用(这里使用restTemplate)微信申请令牌,拿到令牌查询用户信息,将用户信息写入本项目数据库
        XcUser xcUser = wxAuthService.wxAuth(code);
        if(xcUser==null){
            return "redirect:http://www.51xuecheng.cn/error.html";
        }
        String username = xcUser.getUsername();
        //重定向登录页面自动登录    认证的类型:wx
        return "redirect:http://www.51xuecheng.cn/sign.html?username="+username+"&authType=wx";
    }
}

测试获取用户信息
1、在wxAuthService中获取用户信息处打断点

2、进入http://www.51xuecheng.cn/wxsign.html
在这里插入图片描述

3、手机扫码并授权,观察是否成功申请到令牌
在这里插入图片描述

4.3.5 获取用户信息

在WxAuthServiceImpl类中定义获取用户信息方法:
在这里插入图片描述

    /** 携带令牌查询用户信息
     *
     * 请求说明:
     *  http请求方式: GET
     *  https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID
     *
     * 返回说明:
     *{
     * "openid":"OPENID",
     * "nickname":"NICKNAME",
     * "sex":1,
     * "province":"PROVINCE",
     * "city":"CITY",
     * "country":"COUNTRY",
     * "headimgurl": "https://thirdwx.qlogo.cn/mmopen/g3MonUZtNHkdmzicIlibx6iaFqAc56vxLSUfpb6n5WKSYVY0ChQKkiaJSgQ1dZuTOgvLLrhJbERQQ4eMsv84eavHiaiceqxibJxCfHe/0",
     * "privilege":[
     * "PRIVILEGE1",
     * "PRIVILEGE2"
     * ],
     * "unionid": " o6_bmasdasdsad6_2sgVt7hMZOPfL"
     *
     * }
     *
     */
    private Map<String,String> getUserinfo(String access_token,String openid) {

        //url模版
        String wxUrl_template = "https://api.weixin.qq.com/sns/userinfo?access_token=%s&openid=%s";
        //请求微信地址:类似于日志占位符的写法 :令牌    openid用户标识
        String wxUrl = String.format(wxUrl_template, access_token,openid);

        log.info("调用微信接口申请access_token, url:{}", wxUrl);

        //远程调用此url
        ResponseEntity<String> exchange = restTemplate.exchange(wxUrl, HttpMethod.POST, null, String.class);

        //获取响应的结果,防止乱码进行转码(把传进来的ISO_8859_1编码----》UTF_8)
        String result = new  String(exchange.getBody().getBytes(StandardCharsets.ISO_8859_1),StandardCharsets.UTF_8);
        log.info("调用微信接口申请access_token: 返回值:{}", result);
        //将result转化为Map
        Map<String,String> resultMap = JSON.parseObject(result, Map.class);

        return resultMap;
    }

调用获取用户信息。
在这里插入图片描述

    @Override
    public XcUser wxAuth(String code) {

        //1.申请令牌
        Map<String, String> access_token_map = getAccess_token(code);
        System.out.println(access_token_map);
        if(access_token_map==null){
            return null;
        }

        String access_token = access_token_map.get("access_token");//获取令牌
        String openid = access_token_map.get("openid");//获取openid
        //2.携带令牌查询用户信息
        Map<String, String> userinfo = getUserinfo(access_token,openid);
        System.out.println(userinfo);
        if(userinfo==null){
            return null;
        }

        //3.保存用户信息到数据库
        return null;
    }

测试获取用户信息

1、在获取用户信息处打断点

2、进入http://www.51xuecheng.cn/wxsign.html
在这里插入图片描述
3、手机扫码授权
在这里插入图片描述

4.3.6 保存用户信息

向数据库保存用户信息,如果用户不存在将其保存在数据库。

在这里插入图片描述

    @Autowired
    XcUserRoleMapper xcUserRoleMapper;

    /**
     * 保存用户信息到数据库
     * @param userInfo_map
     * @return
     */
    @Transactional
    public XcUser addWxUser(Map userInfo_map){
        //用户表的unionid:在微信开发平台上的唯一id,一个用户扫码只有一个unionid
        String unionid = userInfo_map.get("unionid").toString();
        //根据unionid查询用户信息
        XcUser xcUser = xcUserMapper.selectOne(new LambdaQueryWrapper<XcUser>().eq(XcUser::getWxUnionid, unionid));
        if(xcUser!=null){
            return xcUser;
        }
        //向用户表保存数据:主要是不能为null的字段。
        String userId = UUID.randomUUID().toString();
        xcUser = new XcUser();
        xcUser.setId(userId); //主键
        xcUser.setWxUnionid(unionid);
        xcUser.setNickname(userInfo_map.get("nickname").toString()); //记录从微信得到的昵称
        xcUser.setUserpic(userInfo_map.get("headimgurl").toString());
        xcUser.setName(userInfo_map.get("nickname").toString());
        xcUser.setUsername(unionid);
        xcUser.setPassword(unionid);
        xcUser.setUtype("101001");//学生类型
        xcUser.setStatus("1");//用户状态
        xcUser.setCreateTime(LocalDateTime.now());
        xcUserMapper.insert(xcUser);

        //向用户角色表保存数据
        XcUserRole xcUserRole = new XcUserRole();
        xcUserRole.setId(UUID.randomUUID().toString());//主键
        xcUserRole.setUserId(userId); //userId对应用户表插入数据时生成的主键值,我们当初是uuid生成的
        xcUserRole.setRoleId("17");//学生角色
        xcUserRole.setCreateTime(LocalDateTime.now());
        xcUserRoleMapper.insert(xcUserRole);
        return xcUser;
    }

调用保存用户信息

  • 注意:非事务方法调用事务方法,要使用代理对象调用,前面也提到过这点
  • 这里的addWxUser()方法涉及到了多表操作,所以需要进行事务控制,而wxAuth()是非事务方法,所以这里我们需要注入自身,然后调用addWxUser()

在这里插入图片描述

    // 注入自身
    @Autowired
    WxAuthServiceImpl currentProxy;
 
    @Override
    public XcUser wxAuth(String code) {

        //1.申请令牌
        Map<String, String> access_token_map = getAccess_token(code);
        System.out.println(access_token_map);
        if(access_token_map==null){
            return null;
        }

        String access_token = access_token_map.get("access_token");//获取令牌
        String openid = access_token_map.get("openid");//获取openid
        //2.携带令牌查询用户信息
        Map<String, String> userinfo = getUserinfo(access_token,openid);
        System.out.println(userinfo);
        if(userinfo==null){
            return null;
        }

        //3.保存用户信息到数据库
        XcUser xcUser = currentProxy.addWxUser(userinfo);
        System.out.println(xcUser);
        return xcUser;
    }

测试保存用户信息
1、在保存用户信息处打断点

2、进入http://www.51xuecheng.cn/wxsign.html
在这里插入图片描述
3、手机扫码授权

4、自动跳转到登录页面,提交认证成功。
在这里插入图片描述
在这里插入图片描述

总结:扫码登录执行流程

  • 扫码登录进入到wxLogin方法,在此方法中远程调用wxAuth方法(远程调用微信申请令牌,拿到令牌查询用户信息,将用户信息写入本项目数据库)
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

  • 保存完用户信息到数据库后接着执行重定向
    在这里插入图片描述

  • 重定向进入到我们的统一入口方法
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

5 用户授权

5.1 RBAC

如何实现授权?业界通常基于RBAC实现授权。

RBAC分为两种方式:

  • 基于角色的访问控制(Role-Based Access Control)
  • 基于资源的访问控制(Resource-Based Access Control)

角色的访问控制(Role-Based Access Control)是按角色进行授权,比如:主体的角色为总经理可以查询企业运营报表,查询员工工资信息等,访问控制流程如下:
在这里插入图片描述

根据上图中的判断逻辑,授权代码可表示如下:

if(主体.hasRole("总经理角色id")){
查询工资
}

如果上图中查询工资所需要的角色变化为总经理和部门经理,此时就需要修改判断逻辑为“判断用户的角色是否是总经理或部门经理”,修改代码如下:

if(主体.hasRole("总经理角色id") ||  主体.hasRole("部门经理角色id")){
    查询工资
}

根据上边的例子发现,当需要修改角色的权限时就需要修改授权的相关代码,系统可扩展性差。

基于资源的访问控制(Resource-Based AccessControl)是按资源(或权限)进行授权,比如:用户必须具有查询工资权限才可以查询员工工资信息等,访问控制流程如下:
在这里插入图片描述

根据上图中的判断,授权代码可以表示为:

if(主体.hasPermission("查询工资权限标识")){
    查询工资
}

优点:系统设计时定义好查询工资的权限标识,即使查询工资所需要的角色变化为总经理和部门经理也不需要修改授权代码,系统可扩展性强。

5.2 资源服务授权流程

本项目在资源服务内部进行授权,基于资源的授权模式,因为接口在资源服务,通过在接口处添加授权注解实现授权。

1、首先配置nginx代理
在这里插入图片描述

   #前端开发服务
  upstream uidevserver{
    server 127.0.0.1:8601 weight=10;
  } 
   server {
        listen       80;
        server_name  teacher.51xuecheng.cn;
        #charset koi8-r;
        ssi on;
        ssi_silent_errors on;
        #access_log  logs/host.access.log  main;
        #location / {
         #   alias   D:/itcast2022/xc_edu3.0/code_1/dist/;
         #   index  index.html index.htm;
        #}
        location / {
            proxy_pass   http://uidevserver;
        }

        location /api/ {
                proxy_pass http://gatewayserver/;
        } 
        
        
   }

加载nginx 配置。
在这里插入图片描述

2、在资源服务集成Spring Security

  • 在需要授权的接口处使用@PreAuthorize(“hasAuthority(‘权限标识符’)”)进行控制

  • 下边代码指定/course/list接口需要拥有xc_teachmanager_course_list 权限。
    在这里插入图片描述

  • 设置了@PreAuthorize表示执行此方法需要授权,如果当前用户请求接口没有权限则抛出异常org.springframework.security.access.AccessDeniedException: 不允许访问

说明:

  • 正常情况下需要再定义一份异常处理的方法,但是由于异常处理器是在base模块下,base是所有微服务都依赖的包,直接定义一个处理Spring Security框架抛出的异常就需要引入Security依赖, 一旦引入此依赖就会对服务进行管控,其它的服务会自动完成依赖,所有的url请求都会拦截。
  • 解决:异常处理其中下面捕获不到的一处一定在exception捕获到,所以在exception中处理即可。

3、在统一异常处理处解析此异常信息
在这里插入图片描述

@ResponseBody
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public RestErrorResponse exception(Exception e) {

   log.error("【系统异常】{}",e.getMessage(),e);
   e.printStackTrace();
   if(e.getMessage().equals("不允许访问")){
      return new RestErrorResponse("没有操作此功能的权限");
   }
   return new RestErrorResponse(CommonError.UNKOWN_ERROR.getErrMessage());


}

4、重启资源服务进行测试

使用教学机构用户登录系统

这里使用t1用户登录,账号:t1、密码:111111
在这里插入图片描述

登录成功,点击“教学机构”
在这里插入图片描述

当用户没有权限时页面提示:没有操作此功能的权限。
在这里插入图片描述
授权原理:

  • 每次发送请求都会携带令牌
  • 解析令牌中的权限字段和接口上的权限标识符进行对比,在范围内就通过,不在就拒绝访问。

5.3 授权相关的数据模型

如何给用户分配权限呢?

首先要学习数据模型,本项目授权相关的数据表如下:
在这里插入图片描述

说明如下:

  • xc_user:用户表,存储了系统用户信息,用户类型包括:学生、老师、管理员等
  • xc_user_role:用户角色表,一个用户可拥有多个角色,一个角色可被多个用户所拥有
  • xc_role:角色表,存储了系统的角色信息,学生、老师、教学管理员、系统管理员等。
    • 为了方便给用户分配权限创建的。
  • xc_permission:角色权限表,一个角色可拥有多个权限,一个权限可被多个角色所拥有
  • xc_menu:菜单表,记录了菜单及菜单下的权限
    • 系统所有操作资源的方法

表关系:

  • 用户表----角色表:多对多
  • 角色表----菜单权限表:多对多

流程:

  • 角色和权限进行绑定:通过角色权限表给角色分配对应的权限(比如:老师角色可以访问那些资源方法)
  • 用户和角色进行绑定:同样通过中间表给用户分配对应的角色,此时用户就具有对应的角色权限了。

本项目教学阶段不再实现权限定义及用户权限分配的功能,权限分配的界面原型如下图所示:

在这里插入图片描述

本项目要求掌握基于权限数据模型(5张数据表),要求在数据库中操作完成给用户分配权限、查询用户权限等需求。

1、查询用户所拥有的权限

步骤:

  • 查询用户的id
    在这里插入图片描述

  • 查询用户所拥有的角色
    在这里插入图片描述

  • 查询用户所拥有的权限id
    在这里插入图片描述

  • 根据权限id查询出具体的权限信息。
    在这里插入图片描述

  • 例子:


#需求:查询t1用户对应的权限

#步骤1:先查询t1对应的用户,此时可以得到用户的id=52
select * from xc_user u where u.`username`="t1";

#步骤2:根据用户的id 在用户角色中间表中 查出对应的角色id=20   一个用户有可能是多个角色
select t.role_id from xc_user_role t where t.user_id = "52"

#步骤3:根据角色id 在角色菜单权限中间表中 查出该角色对应的权限id=222222222222222222    一个角色有可能有很多的权限
SELECT p.menu_id FROM xc_permission p where p.role_id ="20"

#步骤4:最终根据权限的id 在权限表中 查询对应的权限信息     
SELECT * FROM xc_menu m WHERE m.id ="222222222222222222"



#优化1:步骤2中一个用户可能拥有多个角色,多个角色id所以要用in(合并步骤2和步骤3,最终查询出角色id对应的权限id)
SELECT p.menu_id FROM xc_permission p WHERE p.role_id in(
	SELECT t.role_id FROM xc_user_role t WHERE t.user_id = "52")
	
#优化2:步骤3中一个角色可能有很多权限,所以多个权限id要用in(合并步骤2和步骤3,最终根据权限id查询出对应的权限信息)
SELECT * FROM xc_menu m where m.id in (
	SELECT p.menu_id FROM xc_permission p WHERE p.role_id IN(
		SELECT t.role_id FROM xc_user_role t WHERE t.user_id = "52"
		)
	)


#总结:
#先查询t1对应的用户,此时可以得到用户的id=52
select * from xc_user u where u.`username`="t1";
#根据用户的id得到对应的角色id,根据角色id得到对应的权限id,根据权限id查询出具体的权限信息。
SELECT * FROM xc_menu m where m.id in (
	SELECT p.menu_id FROM xc_permission p WHERE p.role_id IN(
		SELECT t.role_id FROM xc_user_role t WHERE t.user_id = "52"
		)
	)


在这里插入图片描述

2、给用户分配权限

1)添加权限

  • 查询用户的id
  • 查询权限的id
  • 查询用户的角色,如果没有角色需要先给用户指定角色
  • 向角色权限表添加记录

2)删除用户权限

  • 本项目是基于角色分配权限,如果要删除用户的权限可以给用户换角色,那么新角色下的权限就是用户的权限;如果不换用户的角色可以删除角色下的权限即删除角色权限关系表相应记录,这样操作是将角色下的权限删除,属于该角色的用户都将删除此权限。

5.4 查询用户权限

使用Spring Security进行授权,首先在生成jwt前会查询用户的权限,如下图:
在这里插入图片描述

接下来需要修改UserServiceImpl和PasswordAuthServiceImpl从数据库查询用户的权限。

1、定义mapper接口
在这里插入图片描述

public interface XcMenuMapper extends BaseMapper<XcMenu> {
	//根据用户的id获取对应的权限信息
    @Select("SELECT    * FROM xc_menu WHERE id IN (SELECT menu_id FROM xc_permission WHERE role_id IN ( SELECT role_id FROM xc_user_role WHERE user_id = #{userId} ))")
    List<XcMenu> selectPermissionByUserId(@Param("userId") String userId);
}

2、修改PasswordAuthServiceImpl

修改UserServiceImpl类的getUserPrincipal方法,查询权限信息

最终目的是:把查询到的用户对应的权限信息写入到令牌中,之后发送请求携带令牌和接口上的令牌标识进行对比,在范围内就放行。

在这里插入图片描述

    /**
     * @description 查询用户信息:如果查到了用户拿到正确的密码,最终封装成一个UserDetails对象给spring security框架返回,
     *                         由框架进行密码比对
     * @param user  用户id,主键
     * @return com.xuecheng.ucenter.model.po.XcUser 用户信息
     * @author Mr.M
     * @date 2022/9/29 12:19
     */
    public UserDetails getUserPrincipal(XcUserExt user){
        //取出数据库存储的正确密码
        String password = user.getPassword();
        //根据用户的id查询出对应的权限信息
        List<XcMenu> xcMenus = xcMenuMapper.selectPermissionByUserId(user.getId());
        List<String> permissions = new ArrayList<>();
        if(xcMenus.size()<=0){
            //用户权限,如果不加则报Cannot pass a null GrantedAuthority collection (暂时这里写一个假数据)
            permissions.add("p1");
        }else{
            xcMenus.forEach(menu->{
                //拿到了用户所拥有的权限标识符
                permissions.add(menu.getCode());
            });
        }
        //将用户权限放在XcUserExt中
        user.setPermissions(permissions);
        //将permissions转化为数组
        String[] authorities = permissions.toArray(new String[0]);

        //为了安全在令牌中不放密码
        user.setPassword(null);
        //将user对象转json
        String userString = JSON.toJSONString(user);
        //创建UserDetails对象,权限信息待实现授权功能时再向UserDetail中加入
        UserDetails userDetails = User
                //由原来只存放账号变为存放整个user的信息,并且为了安全
                .withUsername(userString) //user.getUsername()----》userString
                .password(password)
                .authorities(authorities) //数组类型
                .build();
        return userDetails;
    }

5.5 授权测试

以上实现了认证时从数据库查询用户的权限,下边进行用户授权测试。

重启认证服务,使用内容管理课程列表查询为例,代码如下:

在这里插入图片描述

用户拥有xc_teachmanager_course_list权限方可访问课程查询接口。

以用户stu1为例,当它没有此权限时页面报“没有此操作的权限”错误

在这里插入图片描述

将xc_teachmanager_course_list权限分配给用户。

1)首先找到当前用户的角色

2)找到xc_teachmanager_course_list权限的主键

3)在角色权限关系表中添加记录

分配完权限需要重新登录

在这里插入图片描述

由于用户分配了xc_teachmanager_course_list权限,用户具有访问课程查询接口的权限。
在这里插入图片描述
在这里插入图片描述

  • 解析cookie中携带的令牌:确实存入了权限信息
    在这里插入图片描述
    在这里插入图片描述

5.6 细粒度授权

5.6.1 什么是细粒度授权

什么是细粒度授权?

细粒度授权也叫数据范围授权,即不同的用户所拥有的操作权限相同,但是能够操作的数据范围是不一样的。一个例子:用户A和用户B都是教学机构,他们都拥有“我的课程”权限,但是两个用户所查询到的数据是不一样的。

本项目有哪些细粒度授权?

比如:

  • 我的课程,教学机构只允许查询本教学机构下的课程信息。

  • 我的选课,学生只允许查询自己所选课。

如何实现细粒度授权?

  • 细粒度授权涉及到不同的业务逻辑,通常在service层实现,根据不同的用户进行校验,根据不同的参数查询不同的数据或操作不同的数据。

5.6.2 教学机构细粒度授权

教学机构在维护课程时只允许维护本机构的课程,教学机构细粒度授权过程如下:

1)获取当前登录的用户身份

2)得到用户所属教育机构的Id

3)查询该教学机构下的课程信息

  • 最终实现了用户只允许查询自己机构的课程信息。

根据公司Id查询课程,流程如下:

1)教学机构用户登录系统,从用户身份中取出所属机构的id

  • 在用户表中设计了company_id字段存储该用户所属的机构id.

2)接口层取出当前登录用户的身份,取出机构id

3)将机构id传入service方法。

4)service方法将机构id传入Dao方法,最终查询出本机构的课程信息。

代码实现如下:
在这里插入图片描述

    /**
     *
     * @param pageParams  分页查询通用参数
     * @param queryCourseParams  课程查询参数Dto
     * @return
     */
    @ApiOperation("课程查询接口")
    @PreAuthorize("hasAuthority('xc_teachmanager_course_list')")//指定权限标识符,拥有此权限才可以访问此方法
    @PostMapping("/course/list")
    public PageResult<CourseBase> list(PageParams pageParams, @RequestBody(required=false) QueryCourseParamsDto queryCourseParams){

        //取出用户身份
        SecurityUtil.XcUser user = SecurityUtil.getUser();
        //用户所属的机构id
        Long companyId = null;
        if (StringUtils.isNotEmpty(user.getCompanyId())) {
            companyId = Long.parseLong(user.getCompanyId());
        }
        PageResult<CourseBase> pageResult = courseBaseInfoService.queryCourseBaseList(companyId,pageParams, queryCourseParams);
        return pageResult;

    }

Service方法如下:

在这里插入图片描述

@Override
public PageResult<CourseBase> queryCourseBaseList(Long companyId,PageParams pageParams, QueryCourseParamsDto queryCourseParamsDto) {

 //构建查询条件对象
 LambdaQueryWrapper<CourseBase> queryWrapper = new LambdaQueryWrapper<>();
 //机构id
 queryWrapper.eq(CourseBase::getCompanyId,companyId);
 ....

总结:Security 框架无法完成细粒度的授权,只能是在业务层根据不同的机构id拼接不同的sql查询条件,当前用户只能查询出当前用户对应的培训机构课程。

5.6.3 教学机构细粒度授权测试

使用一个教学机构的用户登录项目,并且此用户具有查询课程的权限。

手机修改数据库指定用户归属到一个机构中,涉及以下数据表:

xc_company为机构表

xc_company_user为机构用户关系表

xc_user表中有company_id字段。

我们准备了t1 用户作为此次测试的用户,使用此用户登录系统:

提前在查询课程列表接口处打上断点。

经过测试可以正常取出用户所属的机构id
在这里插入图片描述

跟踪持久层日志发现已将机构id传入dao方法,拼装sql语句,查询本机构的课程信息

在这里插入图片描述

6 实战

6.1 找回密码(实战)

需求:忘记密码需要找回,可以通过手机号找回密码,通过邮箱找回密码以及人工通道。

由于邮件发送比手机验证码要方便的多,所以这里就只做邮件注册、找回密码了

界面访问地址:http://www.51xuecheng.cn/findpassword.html
在这里插入图片描述

接口:

  • 手机验证码:/api/checkcode/phone?param1=手机号
  • 邮箱验证码:/api/checkcode/phone?param1=电子邮箱地址
  • 找回密码:/api/auth/findpassword

请求:

{
     cellphone:'',
     email:'',
     checkcodekey:'',
     checkcode:'',
     confirmpwd:'',
     password:''
 }

响应:

  • 200: 找回成功

  • 其它:找回失败,失败原因使用统一异常处理返回的信息格式

执行流程

1、校验验证码,不一致则抛出异常

2、判断两次密码是否一致,不一致则抛出异常

3、根据手机号和邮箱查询用户

4、如果找到用户更新为新密码

6.2 注册(实战)

需求:为学生提供注册入口,通过此入口注册的用户为学生用户。

界面访问地址:http://www.51xuecheng.cn/register.html
在这里插入图片描述

接口:

  • 手机验证码:/api/checkcode/phone?param1=手机号
  • 注册:/api/auth/register

请求:

{
   cellphone:'',
   username:'',
   email:'',
   nickname:'',
   password:'',
   confirmpwd:'',
   checkcodekey:'',
   checkcode:''
}

响应:

  • 200: 注册成功
  • 其它:注册失败,失败原因使用统一异常处理返回的信息格式

执行流程:

1、校验验证码,如果不一致则抛出异常

2、校验两次密码是否一致,如果不一致则抛出异常

3、校验用户是否存在,如果存在则抛出异常

4、向用户表、用户角色关系表添加数据。角色为学生角色。

  • 13
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值