系统架构
存在两大类的服务器,一种是认证服务器,负责接受令牌的申请请求以及颁发令牌,传统上还会有资源服务器携带令牌在认证服务器进行校验的过程(当然我们的采用的是公钥和私钥的校验机制)
在具体实现上,只要是依赖了oauth的微服务就会通过认证服务器进行校验,而如何进行认证则需要在认证服务器当中进行相关的配置
认证服务器
认证服务器的内容如下,包含oauth与springSecurity的配置类、Controller与Service及其实现、封装的pojo:
配置
-
AuthorizationServerConfig
第一是客户端信息配置,也就是客户端需要有哪些条件才可以访问服务器,比如客户端id和客户端密钥等,可以直接配置到内存中,也可以配置从数据库中读取;
第二是授权服务器端点配置,就是配置认证管理器,令牌存储方式等;
第三个是授权服务器的安全配置,就是配置访问的限制,比如限制校验令牌的配置等。 -
CustomUserAuthenticationConverter
自定义的UserAuthenticationConverter,继承自DefaultUserAuthenticationConverter,重写了convertUserAuthentication方法。默认该方法是获取authentication中的username和权限信息。而我们重写的方法里面还获取了authentication中的principal,判断是不是我们自定义的UserJwt,不是的话就调用userDetailsService.loadUserByUsername去获取,然后将userJwt中的name和id获取出来,添加到返回的map中。
-
UserDetailsServiceImpl
这个是自定义的认证授权类,实现了UserDetailsService接口,并实现了里面的
loadUserByUsername()
方法。这个方法是根据前端传进来的用户名去查出对应的用户信息。然后交给后续的过滤器去进行用户身份的验证。一般这个方法是从数据库中查找用户。 -
WebSecurityConfig
这个是Spring Security的安全配置类。主要配置了某些对于某些请求的限制。在这个类中,还往Spring容器中注入了passwordEncoder和authenticationManagerBean供其他类使用。
Controller与Service及其实现
-
UserLoginController
这个是自定义的一个只使用用户名和密码进行登录的简化的登录方式。
-
LoginService和LoginServiceImpl
UserLoginController的Service层。负责添加一些必要的信息后然后通过RestTemplate模拟浏览器向服务器发送请求获取令牌信息。
pojo
-
AuthToken
封装了Token的相关信息。令牌信息,刷新token,jwt短令牌。
-
CookieUtil
Cookie的工具类。设置Cookie以及根据名称获取Cookie信息。
-
UserJwt
用户信息。实现了UserDetails接口。验证用户时用的就是这个类的对象。
-
changgou.jks
密钥证书,可以使用keytool工具生成。
资源服务授权
资源服务授权
资源服务授权流程
(1)传统授权流程
资源服务器授权流程如上图,客户端先去授权服务器申请令牌,申请令牌后,携带令牌访问资源服务器,资源服务器访问授权服务校验令牌的合法性,授权服务会返回校验结果,如果校验成功会返回用户信息给资源服务器,资源服务器如果接收到的校验结果通过了,则返回资源给客户端。
传统授权方法的问题是用户每次请求资源服务,资源服务都需要携带令牌访问认证服务去校验令牌的合法性,并根 据令牌获取用户的相关信息,性能低下。
(2)公钥私钥授权流程
传统的授权模式性能低下,每次都需要请求授权服务校验令牌合法性,我们可以利用公钥私钥完成对令牌的加密,如果加密解密成功,则表示令牌合法,如果加密解密失败,则表示令牌无效不合法,合法则允许访问资源服务器的资源,解密失败,则不允许访问资源服务器资源。
上图的业务流程如下:
- 客户端请求认证服务申请令牌
- 认证服务生成令牌认证服务采用非对称加密算法,使用私钥生成令牌。
- 客户端携带令牌访问资源服务客户端在Http header 中添加: Authorization:Bearer 令牌。
- 资源服务请求认证服务校验令牌的有效性资源服务接收到令牌,使用公钥校验令牌的合法性。
- 令牌有效,资源服务向客户端响应资源信息
公钥私钥在本项目中是放在了各个服务当中,位置通过yaml进行配置
认证需求分析
认证服务需要实现的功能如下:
1、登录接口
前端post提交账号、密码等,用户身份校验通过,生成令牌,并将令牌写入cookie。
2、退出接口 校验当前用户的身份为合法并且为已登录状态。 将令牌从cookie中删除。
工具封装
在changgou-user-oauth工程中添加如下工具对象,方便操作令牌信息。
创建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<cookieName,cookieValue>
*/
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;
}
}
创建com.changgou.oauth.util.UserJwt类,封装SpringSecurity中User信息以及用户自身基本信息,代码如下:
public class UserJwt extends User {
private String id; //用户ID
private String name; //用户名字
public UserJwt(String username, String password, Collection<? extends GrantedAuthority> authorities) {
super(username, password, authorities);
}
//...get...set
}
业务层
如上图,我们现在实现一个认证流程,用户从页面输入账号密码,到认证服务的Controller层,Controller层调用Service层,Service层调用OAuth2.0的认证地址,进行密码授权认证操作,如果账号密码正确了,就返回令牌信息给Service层,Service将令牌信息给Controller层,Controller层将数据存入到Cookie中,再响应用户。
创建com.changgou.oauth.service.AuthService接口,并添加授权认证方法:
public interface AuthService {
/***
* 授权认证方法
*/
AuthToken login(String username, String password, String clientId, String clientSecret);
}
创建com.changgou.oauth.service.impl.AuthServiceImpl实现类,实现获取令牌数据,这里认证获取令牌采用的是密码授权模式,用的是RestTemplate向OAuth服务发起认证请求,代码如下:
@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);
}
}
控制层
创建控制层com.changgou.oauth.controller.AuthController,编写用户登录授权方法,代码如下:
@RestController
@RequestMapping(value = "/user")
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);
}
}
测试认证接口
使用postman测试:
Post请求:http://localhost:9001/user/login