需求
1、RESTfull风格的鉴权服务(路线相同的情况下根据请求方式鉴别访问权限)
2、包含用户、角色、权限
3、使用JWT最为token认证方式
知识点讲解
方案
传统的单体应用体系下,应用是一个整体,一般针对所有的请求都会进行权限校验。请求一般会通过一个权限的拦截器进行权限的校验,在登录时将用户信息缓存到 session 中,后续访问则从缓存中获取用户信息
但在微服务架构下,一个应用会被拆分成若干个微应用,每个微应用都需要对访问进行鉴权,每个微应用都需要明确当前访问用户以及其权限。尤其当访问来源不只是浏览器,还包括其他服务的调用时,单体应用架构下的鉴权方式就不是特别合适了。因此在设计架构中,要考虑外部应用接入的场景、用户与服务的鉴权、服务与服务的鉴权等多种鉴权场景。
目前主流的方案由四种
- 单点登录(SSO)
一次登入,多地使用。这种方案意味着每个面向用户的服务都必须与认证服务交互,进而产生大量琐碎的网络流量和重复的工作,当动辄数十个微应用时,这种方案的弊端会更加明显。
- 分布式 Session 方案
借助reids或其他共享存储中,将用户认证的信息存储在其中,通常使用用户会话作为 key 来实现的简单分布式哈希映射。当用户访问微服务时,用户数据可以从共享存储中获取。在某些场景下,这种方案很不错,用户登录状态是不透明的。同时也是一个高可用且可扩展的解决方案。这种方案的缺点在于共享存储需要一定保护机制,因此需要通过安全链接来访问,这时解决方案的实现就通常具有相当高的复杂性了。
- 客户端 Token 方案
令牌在客户端生成,由身份验证服务进行签名,并且必须包含足够的信息,以便可以在所有微服务中建立用户身份。令牌会附加到每个请求上,为微服务提供用户身份验证,这种解决方案的安全性相对较好,但身份验证注销是一个大问题,缓解这种情况的方法可以使用短期令牌和频繁检查认证服务等。对于客户端令牌的编码方案,Borsos 更喜欢使用 JSON Web Tokens(JWT),它足够简单且库支持程度也比较好。
- 客户端 Token 与 API 网关结合
这个方案意味着所有请求都通过网关,从而有效地隐藏了微服务。 在请求时,网关将原始用户令牌转换为内部会话 ID 令牌。在这种情况下,注销就不是问题,因为网关可以在注销时撤销用户的令牌。
本文就采用方案4,实现微服务体系中用户鉴权及认证服务。
Token的实现方案业界有多套成熟的方案,这其中最主流的是JWT 和 Oauth2.0 两种方式。
下面就基于JWT的方式具体实现。
SpringSecurity
AuthenticationManager, 用户认证的管理类,所有的认证请求(比如login)都会通过提交一个token给AuthenticationManager的authenticate()方法来实现。当然事情肯定不是它来做,具体校验动作会由AuthenticationManager将请求转发给具体的实现类来做。根据实现反馈的结果再调用具体的Handler来给用户以反馈。
AuthenticationProvider, 认证的具体实现类,一个provider是一种认证方式的实现,比如提交的用户名密码我是通过和DB中查出的user记录做比对实现的,那就有一个DaoProvider;如果我是通过CAS请求单点登录系统实现,那就有一个CASProvider。按照Spring一贯的作风,主流的认证方式它都已经提供了默认实现,比如DAO、LDAP、CAS、OAuth2等。
前面讲了AuthenticationManager只是一个代理接口,真正的认证就是由AuthenticationProvider来做的。一个AuthenticationManager可以包含多个Provider,每个provider通过实现一个support方法来表示自己支持那种Token的认证。AuthenticationManager默认的实现类是ProviderManager。
UserDetailService, 用户认证通过Provider来做,所以Provider需要拿到系统已经保存的认证信息,获取用户信息的接口spring-security抽象成UserDetailService。虽然叫Service,但是我更愿意把它认为是我们系统里经常有的UserDao。
AuthenticationToken, 所有提交给AuthenticationManager的认证请求都会被封装成一个Token的实现,比如最容易理解的UsernamePasswordAuthenticationToken。
SecurityContext,当用户通过认证之后,就会为这个用户生成一个唯一的SecurityContext,里面包含用户的认证信息Authentication。通过SecurityContext我们可以获取到用户的标识Principle和授权信息GrantedAuthrity。在系统的任何地方只要通过SecurityHolder.getSecruityContext()就可以获取到SecurityContext。在Shiro中通过SecurityUtils.getSubject()到达同样的目的。
具体实现
业务流程
- 客户端调用登录接口,传入用户名密码。
- 服务端请求身份认证中心,确认用户名密码正确。
- 服务端创建JWT,返回给客户端。
- 客户端拿到 JWT,进行存储(可以存储在缓存中,也可以存储在数据库中,如果是浏览器,可以存储在 Cookie中)在后续请求中,在 HTTP 请求头中加上 JWT。
- 服务端校验 JWT,校验通过后,返回相关资源和数据。
代码
完整pom文件(项目结构为多模块)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>springcloud</artifactId>
<groupId>com.lhm</groupId>
<version>1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>security</artifactId>
<dependencies>
<!--web 服务-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--security-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
<!--mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.1.0</version>
</dependency>
<!--mybatis-plus日志-->
<dependency>
<groupId>p6spy</groupId>
<artifactId>p6spy</artifactId>
<version>3.8.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- druid的starter -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.9</version>
</dependency>
<!-- redis -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--JSON-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.38</version>
</dependency>
<!-- StringUtils相关工具类jar包 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.4</version>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
认证服务
在登入方面,本次使用了security默认提供的表单登陆方式,因此直接从实现UserDetailsService开始
package com.lhm.springcloud.security.service.impl;
import com.lhm.springcloud.security.