在搭建介绍流程之前,确保您已经搭建了一个 Eureka 注册中心,因为没有注册中心的话会报错(也有可能我搭建的认证服务器是我项目的一个子模块的原因):Request execution error. endpoint=DefaultEndpoint{ serviceUrl='http://localhost:8761/eureka/}
http://localhost:8761/eureka/ 是因为配置文件未提供注册中心地址,springcloud 默认的注册中心地址就是这个
另外:文末会提供所有代码
Oauth2.0 有以下四种授权模式:本文介绍 密码认证
1、授权码模式(Authorization Code)[常用]
2、隐式授权模式(Implicit)[不常用]
3、密码模式(Resource Owner Password Credentials)[常用]
4、客户端模式(Client Credentials)[不常用]
密码认证流程
密码模式(Resource Owner Password Credentials)与授权码模式的区别是申请令牌不再使用授权码,而是直接 通过用户名和密码即可申请令牌。
Post请求:http://localhost:9001/oauth/token
参数:
grant_type:密码模式授权填写password
username:账号
password:密码
并且此链接需要使用 http Basic认证。
所以密码认证流程相对来说比较简单的
前期工作:接下来的截图我默认你已经看过我上一篇博客 授权码认证流程,因为有些代码是在前一篇的基础上
在 UserDetailsServiceImpl 的 loadUserByUsername 方法后面
如下代码块
if(username == null){
return null;
}
// 改成
if(username == null){
return null;
}
String pwd = new BCryptPasswordEncoder().encode("qkm19981013");
String permissions = "permission1,permission2";
//创建User对象
return new UserJwt(username, pwd, AuthorityUtils.commaSeparatedStringToAuthorityList(permissions));
这样我们默认的账户是任意账户名,密码固定为 qkm19981013,接下来使用postman测试
基本上密码认证流程就是这样,但是这有一个问题,我们每次输入我们的用户名和密码的时候,我们都需要带上HttpBasic认证的客户端ID和密钥,但是实际上我们的ID和密钥是不允许透露出去的,这很不安全,所以接下来我们改造这种模式,客户端只需要提供用户名和密码,HttpBasic认证信息由我们服务端提供,步骤流程分为以下几步:
- 用户从页面输入账号密码,请求我们自己的 Controller 接口
- Controller 层调用 Service 层,Service 层调用 OAuth2.0 的认证地址 [/oauth/token]
- 进行密码授权认证操作,如果账号密码正确了,就返回令牌信息给 Service 层
- Service 将令牌信息给 Controller 层
- Controller层将数据存入到Cookie中,再响应用户.
Service 层业务
public interface AuthService {
/**
* 登录,授权认证方法
*/
AuthToken login(String username, String password, String clientId, String clientSecret);
}
@Service
public class AuthServiceImpl implements AuthService {
@Autowired
private LoadBalancerClient loadBalancerClient;
private RestTemplate restTemplate;
private ClientHttpRequestFactory factory;
@Autowired
public void setFactory() {
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setReadTimeout(5000);//ms
factory.setConnectTimeout(15000);//ms
this.factory = factory;
}
@Autowired
public void setRestTemplate() {
this.restTemplate = new RestTemplate(this.factory);
}
@Override
public AuthToken login(String username, String password, String clientId, String clientSecret) {
//申请令牌
return applyToken(username,password,clientId, clientSecret);
}
/**
* 认证方法
* @param username:用户登录名字
* @param password:用户密码
* @param clientId:配置文件中的客户端ID
* @param clientSecret:配置文件中的秘钥
* @return AuthToken
*/
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(@NonNull ClientHttpResponse response) throws IOException {
//当响应的值为400或401时候也要正常响应,不要抛出异常
if (response.getRawStatusCode() != 400 && response.getRawStatusCode() != 401) {
super.handleError(response);
}
}
});
@SuppressWarnings("all")
Map map;
try {
@SuppressWarnings("all")
//http请求spring security的申请令牌接口
ResponseEntity<Map> mapResponseEntity = restTemplate.exchange(path, HttpMethod.POST, new HttpEntity<>(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 客户端ID
* @param clientSecret 客户端密钥
* @return String
*/
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); //注意 "Basic " 英文后面有个空格
}
}
//将客户端id和客户端密码拼接,按“客户端id:客户端密码” 关于为啥这样定义,且采用Base64 编码格式,如下图
- Basic 是明文,没有加密
- 我们将Basic后面的密文拷贝出来,使用 Base64 解密一下看看
Utils 工具
@Data
public class AuthToken implements Serializable{
/**
* 令牌信息
*/
String accessToken;
/**
* 刷新token(refresh_token)
*/
String refreshToken;
/**
* jwt短令牌
*/
String jti;
}
public class CookieUtil {
/**
* 设置cookie
*
* @param response 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 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 (String name : cookieNames) {
if (name.equals(cookieName)) {
cookieMap.put(cookieName, cookieValue);
}
}
}
}
return cookieMap;
}
}
/**
* 继承了 org.springframework.security.core.userdetails.User,
* 相当于对其的扩展,可以写我们需要添加的属性
*/
@SuppressWarnings("unused")
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);
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
VO 对象
@Data
public class LoginVo {
@NotBlank(message = "用户名不能为空")
private String username;
@NotBlank(message = "密码不能为空")
private String password;
}
Controller 层
@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
private AuthService authService;
@PostMapping("/login")
public Result<Object> login(@RequestBody @Validated LoginVo loginVo) {
//申请令牌
AuthToken authToken;
try {
authToken = authService.login(loginVo.getUsername(), loginVo.getPassword(), clientId, clientSecret);
} catch (Exception e) {
return new Result<>(false, StatusCode.ERROR, e.getMessage());
}
//用户身份令牌
String access_token = authToken.getAccessToken();
//将令牌存储到cookie
saveCookie(access_token);
return new Result<>(true, StatusCode.OK, "登录成功!");
}
/**
* 将令牌存储到cookie
*
* @param token token
*/
private void saveCookie(String token) {
HttpServletResponse response = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getResponse();
assert response != null;
CookieUtil.addCookie(response, cookieDomain, "/", "Authorization", token, cookieMaxAge, false);
}
}
代码全部贴完了,接下来我们进行测试
再次发送请求
到此我们仅使用 用户名和密码 就进行了授权操作,保证了服务端的一定的安全性