Sa-Token
很轻量,很简单,甚至可以零配置启动
认证授权其实是两个过程
而我们项目的业务流程分三步:
本项目使用Spring Security框架快速构建认证授权功能体系。
Tip:
基本使用步骤:
OAuth2即OAuth2.0,在OAuth基础上极大简化
OAuth协议是简单的开放的强大的三方授权协议,我们可以通过该协议调用微信三方授权,同样的我们也可以让别的服务调用我们的服务的授权协议,只要我们都遵循OAuth开放授权协议即可
分令牌模式、密码模式、简单模式、客户端模式
以令牌模式(三方协议使用)为例:
令牌模式基本使用:
引入依赖
创建服务配置类和令牌配置类
就可以开放面向第三方认证和授权的协议接口
密码模式为例:
前面说到的令牌模式用的是普通令牌——基于Session,有状态,存储在其他服务器
也就是说第三方认证服务发了一个令牌给我们的服务,我们拿着这个令牌去调用第三方的资源数据(这也是单独的服务),如果是基于Session的普通令牌,那么这个资源服务不知道这个令牌是否有效它还需要去问他自己的认证服务去判断这个令牌是否有效
而往往我们那个令牌之后不只是只访问单个资源,若是普通令牌每访问一个资源那就要请求一次他们的认证服务这将极大耗费性能占用线程,这种微服务下的session的令牌模式也称之为session复制和粘贴
所以我们选用JWT令牌
事实上现在很多微服务体系架构的网站使用的都是JWT令牌——无状态令牌,服务端直接从JWT令牌解析出用户信息
它的特点就是认证服务给了你一个JWT令牌后,你拿着这个JWT令牌去访问其他服务,其他服务不需要再去访问它们自己的认证服务就能知道这个令牌是否有效
JWT令牌的优点:
1、jwt基于json,非常方便解析。
2、可以在令牌中自定义丰富的内容,易扩展。
3、通过非对称加密算法及数字签名技术,JWT防止篡改,安全性高。
4、资源服务使用JWT可不依赖认证服务即可完成授权。
缺点:
1、JWT令牌较长,占存储空间比较大。
JWT令牌由三部分组成,每部分中间使用点(.)分隔,比如:xxxxx.yyyyy.zzzzz
{
"alg": "HS256",
"typ": "JWT"
}
//将上边的内容使用Base64Url编码,得到一个字符串就是JWT令牌的第一部分。
{
"sub": "1234567890",
"name": "456",
"admin": true
}
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
base64UrlEncode(header):jwt令牌的第一部分。
base64UrlEncode(payload):jwt令牌的第二部分。
secret:签名所使用的密钥。
第三部分使用签名算法对第一部分和第二部分的内容进行签名,常用的签名算法是 HS256,常见的还有md5,sha 等,签名算法需要使用密钥进行签名,密钥不对外公开,并且签名是不可逆的,如果第三方更改了内容那么服务器验证签名就会失败,要想保证验证签名正确必须保证内容、密钥与签名前一致。
JWT认证服务和资源服务使用相同的密钥,这叫对称加密,对称加密效率高,如果一旦密钥泄露可以伪造jwt令牌。
JWT还可以使用非对称加密,认证服务自己保留私钥,将公钥下发给受信任的客户端、资源服务,公钥和私钥是配对的,成对的公钥和私钥才可以正常加密和解密,非对称加密效率低但相比对称加密非对称加密更安全一些。
在需要校验的服务里
引入依赖
创建服务配置类和令牌配置类
设置绑定的设备id、服务id、需要鉴权的路径,哪些直接放行等
注意服务id要和认证服务设置的应用资源id要一致
那么接下来就可以回去处理原来机构id硬编码的问题了,
我们可以直接使用Spring Security框架和OAuth协议的上下文对象,从上下文对象中获取令牌并解析令牌得到用户信息
使用SpringCloud自带的gateway-starter起步依赖即可——代替之前老旧的zurl,是新一代网关
我们知道所有的微服务都要经过网关(业务网关gateway),然后经过其路由到各个具体的微服务
Nginx是流量网关,负责流量缓冲,实现反向代理(代理到业务网关gateway、网站门户等)
所以我们可以直接在网关进行认证鉴权(是否携带正确的token)
但是具体的授权在具体的微服务里进行
因为我们每个微服务各司其职,是否拥有权限直接在微服务对外接口的Controller上添加注解控制更为方便且清晰
所以最终:
实现:
小问题:本项目里白名单里的内容都有/open路径标识,该路径指向内容要注意加缓存,不然很容易直接冲击数据库
接下来就从数据库获取真实的用户信息来进行校验了
为了保证用户信息的安全(即便数据库泄露,也能保证账号密码安全),我们使用BCrypt加密算法对密码进行加密后再存储到数据库
而不是直接明文,BCrypt加密算法是不可逆的:
那么BCrypt加密算法是如何验证密码是否正确的呢?
将前端传过来的明文密码和使用数据库里存储的加密密码值中的随机盐部分当做随机盐然后按照相同的加密算法进行加密,若得到的结果和数据库原来存储的密码字符还是一样的结果就证明这个密码是正确的
在配置类中将明文方式更改成BCrypt加密即可
但是有一个问题,那就是User不仅仅只有账号和密码,还有一些其他的信息如头像、生日等信息也是我们需要的
但默认是没有的,所以我们需要扩展SpringSecurity用户信息
在认证阶段DaoAuthenticationProvider会调用UserDetailService查询用户的信息,这里是可以获取到齐全的用户信息的。由于JWT令牌中用户身份信息来源于UserDetails,UserDetails中仅定义了username为用户的身份信息,这里有两个思路:
第一是可以扩展UserDetails,使之包括更多的自定义属性,
第二也可以扩展username的内容 ,比如存入json数据内容作为username的内容。相比较而言,方案二比较简单还不用破坏UserDetails的结构,我们采用方案二。(注意的就是为了安全性,令牌的json数据内容里边要排除密码)
修改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;
/**
* @author Mr.M
* @version 1.0
* @description TODO
* @date 2022/9/28 18:09
*/
@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 {
XcUser user = xcUserMapper.selectOne(new LambdaQueryWrapper<XcUser>().eq);
if(user==null){
//返回空表示用户不存在
return null;
}
//取出数据库存储的正确密码
String password =user.getPassword();
//用户权限,如果不加报Cannot pass a null GrantedAuthority collection
String[] authorities = {"p1"};
//为了安全在令牌中不放密码
user.setPassword(null);
//将user对象转json
String userString = JSON.toJSONString(user);
//创建UserDetails对象
UserDetails userDetails = User.withUsername(userString).password(password).authorities(authorities).build();
return userDetails;
}
}
采取方案二之后,原来直接通过上下文对象自带的方法来获取用户信息的方法就不再适用了,需要做一些转换处理
因为很多地方都有可能调用到所以我们需要将其抽取成一个获取当前用户身份工具类
有很多认证方式:账号密码、微信等第三方认证、手机验证码登录等
为了规范我们应该统一认证入口
只提供一个接口来实现认证,根据参数中的认证类型来实现不同的认证逻辑,而默认来说在SpringSecurity框架内认证是交由一个具体的类和方法来处理的,所以我们定义一个类继承和实现重写excute方法
让他替代原来的处理类和方法,下面是SpringSecurity最基础的流程原理和框架,我们要做的就是重写DaoAuthenticactionProvider和UserDetailService
在该项目具体的代码逻辑里体现了策略模式吧~
在Service注解上添加value属性,给Spring容器里的对象打上了一个标识
在同一认证入口里我们先获取参数里携带的认证方式
然后通过认证方式拼接固定的后缀来获取处理对应认证方式的Bean对象名称
然后获取application这个Spring全局的上下文对象通过指定Bean名称、Bean类型来从容器中获取出指定的Bean,然后通过Bean调用其自己的excute方法处理对应的认证逻辑
通过这种巧妙的方式我们可以省略一些if...else判断,简洁且优雅
在账号密码登录方式,我们还需要加验证码,防止恶意攻击
验证码虽然简单但很多服务都需要使用,所以我们依旧单独做成一个微服务
然后我们依旧在权限认证服务中建立远程调用的Feign客户端,然后写接口和实现类来远程调用验证码微服务
微信扫码需要在微信开放平台注册一个服务
扫码登录流程含义:
首先明确接口规范
引入微信的js资源
在网页中实例化js对象,要包含指定的参数,如
参考前两个的方式来写即可,可以调用阿里云OS短信云服务,其实道理都一样包括邮箱
前面完成了认证,下面开始授权
如何实现授权?业界通常基于RBAC实现授权。
RBAC分为两种方式:
基于角色的访问控制(Role-Based Access Control)
按角色进行授权,比如:主体的角色为总经理可以查询企业运营报表,查询员工工资信息等
基于资源的访问控制(Resource-Based Access Control)⭐️
按资源(或权限)进行授权,比如:用户必须具有查询工资权限才可以查询员工工资信息等
优点:系统设计时定义好查询工资的权限标识,即使查询工资所需要的角色变化为总经理和部门经理也不需要修改授权代码,系统可扩展性强。
下面我们项目就是使用基于资源的访问控制:
在需要访问权限控制的对外controller上添加注解指定需要什么权限即可
那问题来了怎么授予权限?
我们需要解析JWT令牌的权限信息来判断——JWT令牌除了包含用户信息外还应该包含权限信息
先来了解授权相关的数据模型:体会数据库建模 > 经典权限表设计RABC
但是也有一个问题那就是一旦有个别用户的权限比较特殊那就不适用了
好比我和大家都是老师,但我这个老师又因为某些原因要缺少某些权限,我们不可能直接修改角色权限表,因为直接修改角色权限表会导致其他老师也会受牵连
比较直接的解决方法就是用户直接关联权限菜单表,直接给用户绑定权限
前面说到,我们需要解析JWT令牌的权限信息来判断——JWT令牌除了包含用户信息外还应该包含权限信息
那么我们定义mapper、serviceImpl查询记录
然后将查到的权限封装成一个数组在构造令牌的时候将权限数组传入权限构造方法中当做参数即可
上面的授权基本上是大范围的,比如权限就是增删改,但是增删改的范围又是什么呢?
所以根据业务需求和实际情况我们往往需要对数据范围做限定
在我们的项目中,一个机构管理员它是管理员能够怎删改课程,这是框架和JWT令牌配合实现的授权,但是一个机构的管理员他理应只能查看、修改和删除自己机构的课程信息
具体到不同的业务不同的逻辑很难去交由框架去实现,我们根据具体的业务需求和逻辑可以去具体实现的Service中做数据范围的限定
在当前项目场景下我们通过在Service查询的时候拼接SQL通过companyId来where条件判断列出所有符合该机构的数据信息来完成用户数据操作范围的限定