上面流程图 描述了用户要操作的各个微服务,用户查看个人信息 需要访问 用户微服务,下单 需要访问 订单微服务,秒杀抢购商品 需要访问 秒杀微服务。每个服务都需要认证用户的身份,身份认证成功后,需要识别用户的角色,然后授权访问对应的功能,比如管理员 和 普通用户的权限对应的功能 是不一样的。
身份认证
用户身份认证 即 用户去访问系统资源时系统要求验证用户的身份信息,身份合法方可继续访问。常见的用户身份认证表现形式有:用户名密码登录。说通俗点,就相当于 校验用户账号密码是否正确。
用户授权
用户认证通过后去访问系统的资源,系统会判断用户是否拥有访问资源的权限,只允许访问有权限的系统资源,没有权限的资源将无法访问,这个过程叫 用户授权。就是上文说的 “管理员 和 普通用户的权限对应的功能 是不一样的“。
用户访问的项目中,至少有 3 个微服务需要识别用户身份,如果用户访问每个微服务都登录一次就太麻烦了,为了提高用户的体验,我们需要实现 让用户在一个系统中登录,其他任意受信任的系统都可以访问,这个功能就叫 单点登录 。
单点登录(Single Sign On),简称为 SSO,是目前比较流行的企业业务整合的解决方案之一。 单点登录 的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。
随着国内及国外巨头们的平台开放战略以及移动互联网的发展,第三方登录已经不是一个陌生的产品设计概念了。 所谓的第三方登录,是说基于用户在第三方平台上已有的账号和密码来快速完成己方应用的登录或者注册的功能。 而这里的第三方平台,一般是已经拥有大量用户的平台,国外的比如Facebook,Twitter等,国内的比如微博、微信、QQ等。
当需要访问第三方系统的资源时,需要首先通过第三方系统的认证(例如:微信认证),由第三方系统对用户认证通过,并授权资源的访问权限。
分析一个 Oauth2 认证的例子,网站使用微信认证的过程:
可以看到,首先,用户是自己在微信里 信息的资源拥有者;访问登录页面后,开始给网页授权。网页这时会弹出一个二维码页面,资源拥有者 也就是 用户, 扫描二维码,微信会对资源拥有者的身份进行验证, 验证通过后,会询问用户是否给授权网页访问自己的微信数据,用户点击 “确认登录” 表示同意给客户端授权,微信认证服务器会 颁发一个授权码,并重定向到网站。客户端获取到授权码,会携带授权码,向认证服务器申请令牌 ,此过程用户看不到。认证服务器会验证客户端请求的授权码,如果合法,则给客户端颁发令牌,令牌是客户端访问资源的通行证,这样网页就可以访问微信服务器中,用户的基本信息了。 当客户端拿到令牌后,用户在网页中会看到 “已经登录成功”。
Oauth2.0认证流程如下: 引自 Oauth2.0协议 :
rfc6749 https://tools.ietf.org/html/rfc6749
可以看到,对照上面的例子,网页就是 “Client”,换言之,是第三方应用, 资源服务器和认证服务器,可以是一个服务,也可以分开的服务,资源服务器返回受保护资源,如果是分开的,资源服务器 通常要请求 认证服务器 来校验令牌的合法性。
Oauth2 包括以下角色:
(1)客户端,本身不存储资源,需要通过资源拥有者的授权去请求资源服务器的资源,比如:畅购在线 Android 客户端、畅购在线 Web 客户端(浏览器端)、微信客户端等。
(2)资源拥有者,通常为用户,也可以是应用程序,即该资源的拥有者。
(3)授权服务器(也称认证服务器),用来对资源拥有的身份进行认证、对访问资源进行授权。客户端要想访问资源需要通过认证服务器由资源拥有者授 权后方可访问。
(4)资源服务器 存储资源的服务器,比如,畅购网用户管理服务器,存储了畅购网的用户信息等。客户端最终访问资源服务器获取资源信息。
本项目认证服务基于 Spring Security Oauth2 进行构建,并在其基础上作了一些扩展,采用 JWT 令牌机制,并自定义了用户身份信息的内容。
(1)搭建认证服务器
oauth2.0 服务搭建的详细流程,还是比较麻烦的。需要将 changgou-user-oauth 的工程导入到项目中去。
可以看到,配置的客户端 id 和 密钥都是 “changgou”,因为还没有连接数据库,所以用户的密码先设置为 “jia”。
(2)application.yml 配置
(3)启动授权认证服务
启动之前,记得先启动 eureka,再启动该授权认证工程。
OAuth2 授权模式
(1)授权码模式(Authorization Code)
(2)隐式授权模式(Implicit)
(3)密码模式(Resource Owner Password Credentials)
(4)客户端模式(Client Credentials)
其中,授权码模式 和 密码模式 比较常用。
Authorization:Basic WGNXZWJBcHA6WGNXZWJBcHA=WGNXZWJBcHA6WGNXZWJBcHA=
是用户名:密码
的 base64 编码。如果认证失败,服务端会返回 401 Unauthorized。传统授权流程:
可以看到,客户端先去授权服务器申请令牌,申请令牌后,携带令牌访问资源服务器,资源服务器访问授权服务,校验令牌的合法性,授权服务会返回校验结果,如果校验成功,会返回用户信息给资源服务器,资源服务器如果接收到的校验结果通过了,则返回资源给客户端。
传统授权方法的问题是用户每次请求资源服务,资源服务都需要携带令牌访问认证服务去校验令牌的合法性,并根 据令牌获取用户的相关信息,性能低下。
传统的授权模式性能低下,每次都需要请求授权服务校验令牌合法性。可以利用 公钥私钥 完成对令牌的生成和校验:
可以看到,流程如下:
① 客户端请求认证服务申请令牌
② 认证服务生成令牌认证服务采用非对称加密算法,使用私钥生成令牌。
③ 客户端携带令牌访问资源服务,客户端在 Http header 中添加: Authorization:Bearer 令牌。
④ 资源服务请求认证服务校验令牌的有效性资源服务接收到令牌,使用公钥校验令牌的合法性。
⑤ 令牌有效,资源服务向客户端响应资源信息
(✨注意:加密用公钥,解密用私钥;签名用私钥,验证用公钥。)
Spring Security 提供对 JWT 的支持,使用 Spring Security 提供的 JwtHelper 来创建 JWT 令牌,校验 JWT 令牌 等操作。 这里 JWT 令牌,我们采用 非对称算法 进行加密,所以我们要先生成 公钥 和 私钥。
① 生成密钥证书,以下命令采用 RSA 算法,生成密钥证书,每个证书包含 公钥 和 私钥
创建一个文件夹,在该文件夹下执行如下命令行:
keytool -genkeypair -alias changgou -keyalg RSA -keypass changgou -keystore changgou.jks -storepass changgou
Keytool 是一个 Java 提供的证书管理工具
keytool -list -keystore changgou.jks
③ 删除别名
keytool -delete -alias changgou -keystore changgou.jsk
(4) 导出公钥
openssl 是一个加解密工具包,这里使用 openssl 来导出公钥信息。
安装 openssl:http://slproweb.com/products/Win32OpenSSL.html
安装 Win64OpenSSL-1_1_0g.exe
配置 openssl 的 path 环境变量:
cmd 进入 changgou.jks 文件所在目录执行如下命令:
keytool -list -rfc --keystore changgou.jks | openssl x509 -inform pem -pubkey
将上边的公钥拷贝到文本 public.key 文件中,合并为一行,将它放到需要实现授权认证的工程中:
(1)创建令牌数据
在 changgou-user-oauth 工程中创建测试类 com.changgou.token.CreateJwtTest,使用它来创建令牌信息,代码如下:
/**
* 令牌的创建和解析
*/
public class CreateJWTTest {
/**
* 创建令牌,用私钥加盐,加密算法是非对称加密
*/
@Test
public void testCreateToken(){
// 加载证书
ClassPathResource resource=new ClassPathResource("changgou.jks");
// 读取证书数据
KeyStoreKeyFactory keyStoreKeyFactory=new KeyStoreKeyFactory(resource,"changgou".toCharArray());
// 获取一对密钥
KeyPair keyPair = keyStoreKeyFactory.getKeyPair("changgou","changgou".toCharArray());
// 获取私钥
RSAPrivateKey aPrivate = (RSAPrivateKey) keyPair.getPrivate();
// 创建令牌,使用私钥加盐,非对称加密
Map<String,Object> payload=new HashMap<>();
payload.put("key1","value1");
payload.put("key2","value2");
payload.put("key3","value3");
Jwt jwt = JwtHelper.encode(JSON.toJSONString(payload), new RsaSigner(aPrivate));
// 获取令牌
String token=jwt.getEncoded();
System.out.println(token);
}
}
运行结果:
(2)解析令牌
创建令牌后,可以对 JWT 令牌进行解析,这里解析需要用到公钥,我们可以将之前生成的公钥 public.key 拷贝出来,用字符串变量 token 存储,然后通过公钥进行验证。
在 changgou-user-oauth 创建测试类 com.changgou.token.ParseJwtTest 实现解析校验令牌数据,代码如下:
public class ParseJwtTest {
/***
* 解析令牌
*/
@Test
public void testParseToken(){
//令牌
String token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkxIjoidmFsdWUxIiwia2V5MiI6InZhbHVlMiIsImtleTMiOiJ2YWx1ZTMifQ.M2-nMoL6drCxN2D-4bD7tUzBZK7FQwEVQ_677fmRUVfe7AOX4fUErfvBvZ3TXkKIvflWgE1ESnxJ0GYKaLre4qLn8JMyd0f6z1TGZ_0RQS5PJER8DcMxELtGtfllAjOa2wlM1Ui9pfB1IghlnRZzwsUma3sVwasOy9iIa4fiG8nI7MUDqaZSzWReO7IGKNvvIXvnW7NMKeFUolJDd84SNR-mLoJfaIH7Temcch0Xhk9ci01ly41eeOyJOvkzVlJQTNojwZGp3yL8QGA_hkxjw7LM3jViaJA0v-7NHVkkcEhvPsRHH2P4Xo3i2wfLcFonOsvnXhMTnr74_7j83gFh0w";
//公钥
String publickey = "-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAhCDQMSUvJ+5YcpTCPijbPWhuot8tOm2IPSZa96Mnr561Y6v4ckJejf58GneFrdn6eBaYXaoo4Cyge9LYzevHpPijBO4cnCt3dCBf2k1c/7adbhwfwVAE9sUsYSFbgOq4mobYXIMwcbdNeO0+Z+AVhUhv+nEBS+fUNkdV55WwvRDKG3pnuNnyMMBDj0XclJjDOfz2NNGignsVIiefPXhE0OdkAL6vIX89U9G5wUUbL87aPOCrvqEpF4jJKyDPQa1bRVATOo8EFWSmkhiAzTQwATvq716ZTTzpjrXJRQ/m4jNuSl0OT0rIDfjxjlfg1shQTXBCW/kHxmaCZ1BQrBQqywIDAQAB-----END PUBLIC KEY-----";
//校验Jwt
Jwt jwt = JwtHelper.decodeAndVerify(token, new RsaVerifier(publickey));
//获取Jwt原始内容 载荷
String claims = jwt.getClaims();
System.out.println(claims);
//jwt令牌
String encoded = jwt.getEncoded();
System.out.println(encoded);
}
}
运行结果:
执行流程:
(1)用户登录,请求认证服务
(2)认证服务认证通过,生成 jwt 令牌,将 jwt 令牌及相关信息写入 cookie
(3)用户访问资源页面,带着 cookie 到网关
(4)网关从 cookie 获取 token,如果存在 token,则校验 token 合法性,如果不合法则拒绝访问,否则放行
(5)用户退出,请求认证服务,删除 cookie 中的 token
认证服务需要实现的功能如下:
UserDetails 是用户信息的载体,其实就是令牌的载荷。
在 changgou-user-oauth 工程中添加如下工具对象,方便操作令牌信息。
(1)创建com.changgou.oauth.util.AuthToken 类,存储用户令牌数据,代码如下:
public class AuthToken implements Serializable{
//令牌信息
String accessToken;
//刷新token(refresh_token)
String refreshToken;
//jwt短令牌
String jti;
//...get...set
}
创建com.changgou.oauth.util.CookieUtil类,操作Cookie,代码如下:
public class CookieUtil {
/**
* 设置cookie
*
* @param response
* @param name cookie名字
* @param value cookie值
* @param maxAge cookie生命周期 以秒为单位
*/
public static void addCookie(HttpServletResponse response, String domain, String path, String name,
String value, int maxAge, boolean httpOnly) {
Cookie cookie = new Cookie(name, value);
cookie.setDomain(domain);
cookie.setPath(path);
cookie.setMaxAge(maxAge);
cookie.setHttpOnly(httpOnly);
response.addCookie(cookie);
}
/**
* 根据cookie名称读取cookie
* @param request
* @return map
*/
public static Map<String,String> readCookie(HttpServletRequest request, String ... cookieNames) {
Map<String,String> cookieMap = new HashMap<String,String>();
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
String cookieName = cookie.getName();
String cookieValue = cookie.getValue();
for(int i=0;i<cookieNames.length;i++){
if(cookieNames[i].equals(cookieName)){
cookieMap.put(cookieName,cookieValue);
}
}
}
}
return cookieMap;
}
}
(2)创建 com.changgou.oauth.util.UserJwt 类,封装 SpringSecurity 中 User 信息以及用户自身基本信息,之后放到令牌的载荷中:
public class UserJwt extends User {
private String id; //用户ID
private String name; //用户名字
private String company; //公司
private String address; //地址
//...get...set
}
可以看到,我们现在实现一个认证流程,用户从页面输入账号密码,到认证服务的 Controller层,Controller 层调用 Service层,Service 层调用 OAuth2.0 的认证地址,进行密码授权认证操作,如果账号密码正确了,就返回令牌信息给 Service 层,Service 将令牌信息给 Controller 层,Controller 层将数据存入到 Cookie 中,再响应用户。使用密码授权,和之前用 postman 测试密码授权模式的实现 过程是相似的, 不同的是,因为用户是不知道 客户端 id 和 密码的,所以用户只需要输入 账号、密码,授权方式 grant_type=password、客户端 id 和密码(通过 BASE64 编码 )都需要程序给。
先提供登录的 Service 接口:
public interface LoginService {
/**
* 模拟用户的行为 发送请求 申请令牌 返回
*/
AuthToken login(String username, String password, String clientId, String clientSecret, String grandType);
}
实现:
@Service
public class LoginServiceImpl implements LoginService {
@Autowired
private RestTemplate restTemplate;
@Autowired
private LoadBalancerClient loadBalancerClient;
@Override
public AuthToken login(String username, String password, String clientId, String clientSecret, String grandType) {
//1.定义url (申请令牌的url)
//参数 : 微服务的名称spring.appplication指定的名称
ServiceInstance choose = loadBalancerClient.choose("user-auth");
String url =choose.getUri().toString()+"/oauth/token";
//2.定义头信息 (有client id 和client secr)
MultiValueMap<String,String> headers = new LinkedMultiValueMap<>();
headers.add("Authorization","Basic "+Base64.getEncoder().encodeToString(new String(clientId+":"+clientSecret).getBytes()));
//3. 定义请求体 有授权模式 用户的名称 和密码
MultiValueMap<String,String> formData = new LinkedMultiValueMap<>();
formData.add("grant_type",grandType);
formData.add("username",username);
formData.add("password",password);
//4.模拟浏览器 发送POST 请求 携带 头 和请求体 到认证服务器
/**
* 参数1 指定要发送的请求的url
* 参数2 指定要发送的请求的方法 PSOT
* 参数3 指定请求实体(包含头和请求体数据)
*/
HttpEntity<MultiValueMap> requestentity = new HttpEntity<MultiValueMap>(formData,headers);
ResponseEntity<Map> responseEntity = restTemplate.exchange(url, HttpMethod.POST, requestentity, Map.class);
//5.接收到返回的响应(就是:令牌的信息)
Map body = responseEntity.getBody();
//封装一次.
AuthToken authToken = new AuthToken();
//访问令牌(jwt)
String accessToken = (String) body.get("access_token");
//刷新令牌(jwt)
String refreshToken = (String) body.get("refresh_token");
//jti,作为用户的身份标识
String jwtToken= (String) body.get("jti");
authToken.setJti(jwtToken);
authToken.setAccessToken(accessToken);
authToken.setRefreshToken(refreshToken);
//6.返回
return authToken;
}
这里使用 LoadBalancerClient 是为了从微服务的配置中获取 url 等信息,定义请求体,就对应着之前密码授权实现中 请求的 grant_type、username、password 参数;头信息也是选择 Basic Auth 后,生成的 VAULE ,其实就是 ”Basic“ 和 客户端id 、密码 Base64 编码后的字符串 拼接起来的值:
提供登录的 Controller 层:
@RestController
@RequestMapping("/user")
public class UserLoginController {
@Autowired
private LoginService loginService;
@Value("${auth.clientId}")
private String clientId;
@Value("${auth.clientSecret}")
private String clientSecret;
private static final String GRAND_TYPE = "password";//授权模式 密码模式
@Value("${auth.cookieDomain}")
private String cookieDomain;
//Cookie生命周期
@Value("${auth.cookieMaxAge}")
private int cookieMaxAge;
/**
* 密码模式 认证.
*
* @param username
* @param password
* @return
*/
@RequestMapping("/login")
public Result<Map> login(String username, String password) {
//登录 之后生成令牌的数据返回
AuthToken authToken = loginService.login(username, password, clientId, clientSecret, GRAND_TYPE);
//设置到cookie中
saveCookie(authToken.getAccessToken());
return new Result<>(true, StatusCode.OK,"令牌生成成功",authToken);
}
private void saveCookie(String token){
HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
CookieUtil.addCookie(response,cookieDomain,"/","Authorization",token,cookieMaxAge,false);
}
}
运行结果:
创建 com.changgou.oauth.service.AuthService 接口,并添加授权认证方法:
public interface AuthService {
/***
* 授权认证方法
*/
AuthToken login(String username, String password, String clientId, String clientSecret);
}
实现:
@Service
public class AuthServiceImpl implements AuthService {
@Autowired
private LoadBalancerClient loadBalancerClient;
@Autowired
private RestTemplate restTemplate;
/***
* 授权认证方法
* @param username
* @param password
* @param clientId
* @param clientSecret
* @return
*/
@Override
public AuthToken login(String username, String password, String clientId, String clientSecret) {
//申请令牌
AuthToken authToken = applyToken(username,password,clientId, clientSecret);
if(authToken == null){
throw new RuntimeException("申请令牌失败");
}
return authToken;
}
/****
* 认证方法
* @param username:用户登录名字
* @param password:用户密码
* @param clientId:配置文件中的客户端ID
* @param clientSecret:配置文件中的秘钥
* @return
*/
private AuthToken applyToken(String username, String password, String clientId, String clientSecret) {
//选中认证服务的地址
ServiceInstance serviceInstance = loadBalancerClient.choose("user-auth");
if (serviceInstance == null) {
throw new RuntimeException("找不到对应的服务");
}
//获取令牌的url
String path = serviceInstance.getUri().toString() + "/oauth/token";
//定义body
MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
//授权方式
formData.add("grant_type", "password");
//账号
formData.add("username", username);
//密码
formData.add("password", password);
//定义头
MultiValueMap<String, String> header = new LinkedMultiValueMap<>();
header.add("Authorization", httpbasic(clientId, clientSecret));
//指定 restTemplate当遇到400或401响应时候也不要抛出异常,也要正常返回值
restTemplate.setErrorHandler(new DefaultResponseErrorHandler() {
@Override
public void handleError(ClientHttpResponse response) throws IOException {
//当响应的值为400或401时候也要正常响应,不要抛出异常
if (response.getRawStatusCode() != 400 && response.getRawStatusCode() != 401) {
super.handleError(response);
}
}
});
Map map = null;
try {
//http请求spring security的申请令牌接口
ResponseEntity<Map> mapResponseEntity = restTemplate.exchange(path, HttpMethod.POST,new HttpEntity<MultiValueMap<String, String>>(formData, header), Map.class);
//获取响应数据
map = mapResponseEntity.getBody();
} catch (RestClientException e) {
throw new RuntimeException(e);
}
if(map == null || map.get("access_token") == null || map.get("refresh_token") == null || map.get("jti") == null) {
//jti是jwt令牌的唯一标识作为用户身份令牌
throw new RuntimeException("创建令牌失败!");
}
//将响应数据封装成AuthToken对象
AuthToken authToken = new AuthToken();
//访问令牌(jwt)
String accessToken = (String) map.get("access_token");
//刷新令牌(jwt)
String refreshToken = (String) map.get("refresh_token");
//jti,作为用户的身份标识
String jwtToken= (String) map.get("jti");
authToken.setJti(jwtToken);
authToken.setAccessToken(accessToken);
authToken.setRefreshToken(refreshToken);
return authToken;
}
/***
* base64编码
* @param clientId
* @param clientSecret
* @return
*/
private String httpbasic(String clientId,String clientSecret){
//将客户端id和客户端密码拼接,按“客户端id:客户端密码”
String string = clientId+":"+clientSecret;
//进行base64编码
byte[] encode = Base64Utils.encode(string.getBytes());
return "Basic "+new String(encode);
}
}
可以看到,实现获取令牌数据,这里认证获取令牌采用的是密码授权模式,用的是RestTemplate 向 OAuth 服务发起认证请求。
提供 AuthController,编写用户登录授权方法:
@RestController
@RequestMapping(value = "/userx")
public class AuthController {
//客户端ID
@Value("${auth.clientId}")
private String clientId;
//秘钥
@Value("${auth.clientSecret}")
private String clientSecret;
//Cookie存储的域名
@Value("${auth.cookieDomain}")
private String cookieDomain;
//Cookie生命周期
@Value("${auth.cookieMaxAge}")
private int cookieMaxAge;
@Autowired
AuthService authService;
@PostMapping("/login")
public Result login(String username, String password) {
if(StringUtils.isEmpty(username)){
throw new RuntimeException("用户名不允许为空");
}
if(StringUtils.isEmpty(password)){
throw new RuntimeException("密码不允许为空");
}
//申请令牌
AuthToken authToken = authService.login(username,password,clientId,clientSecret);
//用户身份令牌
String access_token = authToken.getAccessToken();
//将令牌存储到cookie
saveCookie(access_token);
return new Result(true, StatusCode.OK,"登录成功!");
}
/***
* 将令牌存储到cookie
* @param token
*/
private void saveCookie(String token){
HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
CookieUtil.addCookie(response,cookieDomain,"/","Authorization",token,cookieMaxAge,false);
}
}
(1)认证主要是认证用户身份是否合法,合法的话才能继续访问系统,常见的认证方式,是校验用户的账号、密码是否正确。
(2)授权是系统允许用户访问资源,比如 管理员 和 普通用户访问资源的权限是不一样的。
(3)单点登录是 在多个应用系统中,用户只需要登录一次,就可以访问所有互相信任的应用系统。
分布式系统如果想实现单点登录,需要提供认证系统,用户第一次登录时,到认证系统中进行用户名和密码的认证,通过认证的话,可以把令牌 token 作为 key,用户信息作为 value,存在 Redis 中。之后用户再访问受信任的其他微服务时,都是带着令牌的,认证系统会到 Redis 中校验令牌是否有效,有效则可以访问该服务。就是说,用户想要访问受信任的系统,认证系统就会校验令牌的,通过的话才能访问。
(4)第三方登录是基于用户在第三方平台上已有的账号、密码 来快速完成登录或注册功能。
(5)OAuth 是一个开放授权的标准,允许用户授权第三方移动应用,访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方移动应用。
OAuth 有几种授权模式,常用的是 授权码模式、密码模式。
授权码模式举例:
比如,我是用户,我授权某应用访问微信上的信息。在我访问登录页面后,应用会询问我是否授权访问我的微信数据,我确认授权后,微信认证服务器会颁发一个授权码,并重定向到页面,应用就获取到授权码,接下来会携带授权码,向微信认证服务器申请令牌,认证服务器会验证授权码,如果合法,就颁发令牌,这样网页就能访问到我的微信信息了,我也就“登录成功”了。(这里有几个问题:
① 访问服务上的信息有什么用?
比如获取用户信息——昵称和头像,就可以登录;比如获取好友信息,之后可以分享给好友。
② 为什么微信认证服务器不直接发给应用令牌呢?
因为默认浏览器是不安全的,应用用授权码换令牌的过程是访问微信认证服务器,而不是通过浏览器,也就是说,不能让浏览器拿到令牌。
③ 为什么要给个令牌,而不是直接访问资源呢?
因为以后应用可能还要访问资源,之后可以重用这个令牌。)
通过“令牌”,可以实现授权的功能,又保证安全可控,这就是 Oauth 2.0 的优点。
(6)传统的授权中存在一个问题,就是 每次请求资源服务时,都需要携带令牌,认证服务每次都需要校验令牌的合法性,性能低下,本项目采用私钥签名,公钥验证的方式。(公钥是随意发布,大家都可见的;私钥是仅自己可见的。加密的话,一定是希望只有我自己能解密,其他人解密不了,所以是公钥加密,私钥解密;签名的话,一定是希望只有我能发布这个签名,别人冒充不了,所以是私钥签名,公钥验证。)
本项目基于 Spring security+Oauth2.0 完成用户认证和授权,并自定义了用户身份信息内容。
Spring security 是一个身份验证和访问控制框架,它集成了 Oauth 2.0 协议。Oauth2.0协议的核心是颁发令牌(和校验令牌)。
搭建好服务,使用 @EnableAuthorizationServer ,模拟AuthorizationServer,因为 Spring security 提供了对 JWT 的支持,所以可以生成符合JWT规范的令牌, 先使用 Java 提供的证书管理工具 Keytool ,生成密钥证书 changgou.jks,证书里包含公钥和私钥,把它放在项目的 resources 下,并把它的路径作为 applicaiton.yml 中 encrypt: key-store: location:
属性的值,这样后续可以使用私钥加盐、非对称加密算法,创建 JWT 令牌。 openssl 可以导出公钥信息,之后可以使用 @EnableResourceServer(见下一篇博客) ,使用公钥来验证令牌。
本项目中,用户登录时,认证服务采用密码授权的方式,获取到 access_token 访问令牌、refresh_token 刷新令牌、过期时间、令牌唯一标识 jti 信息。再把令牌写入 cookie 和 Http headers 中。后续访问资源页面时,网关会将 Http headers 中的令牌信息进行传递(cookie 也在的)。获取到令牌之后,可以把它存在 Redis 中,之后网关从 Http Headers 中获取令牌后,看看 Redis 中是否有这条记录,令牌相当于入场券,有令牌记录说明用户被授予入场券了,允以访问。(这只是个思路,实际项目中并没有实现,项目中主要是在令牌中使用角色信息作载荷,用Authorization Server 配合具体的方法做角色校验,见下一篇博客。)