三方登录认证模块,实现华为账号免密登录
This commit is contained in:
parent
10c746c4f9
commit
666575770a
10
pom.xml
10
pom.xml
@ -161,6 +161,13 @@
|
||||
<version>${sf.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 三方授权模块-->
|
||||
<dependency>
|
||||
<groupId>com.smarterFramework</groupId>
|
||||
<artifactId>sf-oauth</artifactId>
|
||||
<version>${sf.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 通用工具-->
|
||||
<dependency>
|
||||
<groupId>com.smarterFramework</groupId>
|
||||
@ -200,7 +207,8 @@
|
||||
<module>sf-file</module>
|
||||
<module>sf-common</module>
|
||||
<module>sf-apijson</module>
|
||||
</modules>
|
||||
<module>sf-oauth</module>
|
||||
</modules>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
<build>
|
||||
|
@ -60,6 +60,12 @@
|
||||
<groupId>com.smarterFramework</groupId>
|
||||
<artifactId>sf-file</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.smarterFramework</groupId>
|
||||
<artifactId>sf-oauth</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.smarterFramework</groupId>
|
||||
<artifactId>sf-order</artifactId>
|
||||
|
@ -48,9 +48,10 @@ public class SysLoginController
|
||||
public AjaxResult login(@RequestBody LoginBody loginBody, HttpSession session)
|
||||
{
|
||||
AjaxResult ajax = AjaxResult.success();
|
||||
loginService.validateCaptcha(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode());
|
||||
// 生成令牌
|
||||
String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(),
|
||||
loginBody.getUuid(), session);
|
||||
String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(),
|
||||
session);
|
||||
ajax.put(Constants.TOKEN, token);
|
||||
return ajax;
|
||||
}
|
||||
|
@ -112,7 +112,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter
|
||||
// 过滤请求
|
||||
.authorizeRequests()
|
||||
// 对于登录login 注册register 验证码captchaImage 允许匿名访问
|
||||
.antMatchers("/login", "/register", "/captchaImage").permitAll()
|
||||
.antMatchers("/login", "/register", "/captchaImage","/oauth/**").permitAll()
|
||||
// 静态资源,可匿名访问
|
||||
.antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll()
|
||||
.antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll()
|
||||
|
@ -1,6 +1,7 @@
|
||||
package com.sf.framework.web.service;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.security.auth.message.AuthException;
|
||||
import javax.servlet.http.HttpSession;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@ -32,6 +33,7 @@ import com.sf.framework.manager.factory.AsyncFactory;
|
||||
import com.sf.framework.security.context.AuthenticationContextHolder;
|
||||
import com.sf.system.service.ISysConfigService;
|
||||
import com.sf.system.service.ISysUserService;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* 登录校验方法
|
||||
@ -56,19 +58,18 @@ public class SysLoginService
|
||||
@Autowired
|
||||
private ISysConfigService configService;
|
||||
|
||||
@Autowired
|
||||
private SysPermissionService permissionService;
|
||||
|
||||
/**
|
||||
* 登录验证
|
||||
*
|
||||
*
|
||||
* @param username 用户名
|
||||
* @param password 密码
|
||||
* @param code 验证码
|
||||
* @param uuid 唯一标识
|
||||
* @return 结果
|
||||
*/
|
||||
public String login(String username, String password, String code, String uuid, HttpSession session)
|
||||
public String login(String username, String password, HttpSession session)
|
||||
{
|
||||
// 验证码校验
|
||||
validateCaptcha(username, code, uuid);
|
||||
// 登录前置校验
|
||||
loginPreCheck(username, password);
|
||||
// 用户验证
|
||||
@ -105,6 +106,25 @@ public class SysLoginService
|
||||
return tokenService.createToken(loginUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* 无密码登录
|
||||
* @param userName
|
||||
* @return
|
||||
* @author zoukun
|
||||
*/
|
||||
public String noPwdLogin(String userName, HttpSession session){
|
||||
SysUser user = userService.selectUserByUserName(userName);
|
||||
Assert.notNull(user,MessageUtils.message("user.not.exists"));
|
||||
LoginUser loginUser = new LoginUser(user.getUserId(), user.getDeptId(), user, permissionService.getMenuPermission(user));
|
||||
// 记录登陆信息
|
||||
AsyncManager.me().execute(AsyncFactory.recordLogininfor(userName, Constants.LOGIN_SUCCESS,
|
||||
MessageUtils.message("user.login.success")));
|
||||
recordLoginInfo(loginUser.getUserId());
|
||||
ApijsonUtils.buildFormSession(session, String.valueOf(loginUser.getUserId()));
|
||||
return tokenService.createToken(loginUser);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 校验验证码
|
||||
*
|
||||
|
@ -1,6 +1,8 @@
|
||||
package com.sf.framework.web.service;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.security.core.Authentication;
|
||||
@ -23,6 +25,7 @@ import com.sf.framework.security.context.AuthenticationContextHolder;
|
||||
*
|
||||
* @author ztzh
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class SysPasswordService
|
||||
{
|
||||
|
32
sf-oauth/pom.xml
Normal file
32
sf-oauth/pom.xml
Normal file
@ -0,0 +1,32 @@
|
||||
<?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>smarterFramework</artifactId>
|
||||
<groupId>com.smarterFramework</groupId>
|
||||
<version>1.0.0</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>sf-oauth</artifactId>
|
||||
|
||||
<description>
|
||||
oauth三方登录认证模块
|
||||
</description>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<!-- 通用工具-->
|
||||
<dependency>
|
||||
<groupId>com.smarterFramework</groupId>
|
||||
<artifactId>sf-common</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.smarterFramework</groupId>
|
||||
<artifactId>sf-framework</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
41
sf-oauth/src/main/java/com/sf/oauth/config/AuthConfig.java
Normal file
41
sf-oauth/src/main/java/com/sf/oauth/config/AuthConfig.java
Normal file
@ -0,0 +1,41 @@
|
||||
package com.sf.oauth.config;
|
||||
|
||||
import lombok.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 配置类
|
||||
*
|
||||
* @author zoukun
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class AuthConfig {
|
||||
|
||||
/**
|
||||
* 客户端id:对应各平台的appKey
|
||||
*/
|
||||
private String clientId;
|
||||
|
||||
/**
|
||||
* 客户端Secret:对应各平台的appSecret
|
||||
*/
|
||||
private String clientSecret;
|
||||
|
||||
/**
|
||||
* 登录成功后的回调地址
|
||||
*/
|
||||
private String redirectUri;
|
||||
|
||||
/**
|
||||
* 支持自定义授权平台的 scope 内容
|
||||
*/
|
||||
private List<String> scopes;
|
||||
|
||||
private boolean ignoreCheckState;
|
||||
|
||||
}
|
76
sf-oauth/src/main/java/com/sf/oauth/config/AuthSource.java
Normal file
76
sf-oauth/src/main/java/com/sf/oauth/config/AuthSource.java
Normal file
@ -0,0 +1,76 @@
|
||||
package com.sf.oauth.config;
|
||||
|
||||
import com.sf.oauth.exception.AuthException;
|
||||
import com.sf.oauth.request.AuthDefaultRequest;
|
||||
|
||||
/**
|
||||
* OAuth平台的API地址的统一接口,提供以下方法:
|
||||
* 1) {@link AuthSource#authorize()}: 获取授权url. 必须实现
|
||||
* 2) {@link AuthSource#accessToken()}: 获取accessToken的url. 必须实现
|
||||
* 3) {@link AuthSource#userInfo()}: 获取用户信息的url. 必须实现
|
||||
* 4) {@link AuthSource#revoke()}: 获取取消授权的url. 非必须实现接口(部分平台不支持)
|
||||
* 5) {@link AuthSource#refresh()}: 获取刷新授权的url. 非必须实现接口(部分平台不支持)
|
||||
* <p>
|
||||
*
|
||||
* @author zoukun
|
||||
*/
|
||||
public interface AuthSource {
|
||||
|
||||
/**
|
||||
* 授权的api
|
||||
*
|
||||
* @return url
|
||||
*/
|
||||
String authorize();
|
||||
|
||||
/**
|
||||
* 获取accessToken的api
|
||||
*
|
||||
* @return url
|
||||
*/
|
||||
String accessToken();
|
||||
|
||||
/**
|
||||
* 获取用户信息的api
|
||||
*
|
||||
* @return url
|
||||
*/
|
||||
String userInfo();
|
||||
|
||||
/**
|
||||
* 取消授权的api
|
||||
*
|
||||
* @return url
|
||||
*/
|
||||
default String revoke() {
|
||||
throw new AuthException("UNSUPPORTED");
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新授权的api
|
||||
*
|
||||
* @return url
|
||||
*/
|
||||
default String refresh() {
|
||||
throw new AuthException("UNSUPPORTED");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Source的字符串名字
|
||||
*
|
||||
* @return name
|
||||
*/
|
||||
default String getName() {
|
||||
if (this instanceof Enum) {
|
||||
return String.valueOf(this);
|
||||
}
|
||||
return this.getClass().getSimpleName();
|
||||
}
|
||||
|
||||
/**
|
||||
* 平台对应的 AuthRequest 实现类,必须继承自 {@link AuthDefaultRequest}
|
||||
*
|
||||
* @return class
|
||||
*/
|
||||
Class<? extends AuthDefaultRequest> getTargetClass();
|
||||
}
|
@ -0,0 +1,143 @@
|
||||
package com.sf.oauth.controller;
|
||||
|
||||
import cn.hutool.core.lang.UUID;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.sf.common.constant.Constants;
|
||||
import com.sf.common.constant.HttpStatus;
|
||||
import com.sf.common.core.domain.AjaxResult;
|
||||
import com.sf.oauth.config.AuthConfig;
|
||||
import com.sf.oauth.domain.AuthCallback;
|
||||
import com.sf.oauth.domain.AuthUser;
|
||||
import com.sf.oauth.enums.scope.AuthHuaweiScope;
|
||||
import com.sf.oauth.exception.AuthException;
|
||||
import com.sf.oauth.request.AuthHuaweiRequest;
|
||||
import com.sf.oauth.request.AuthRequest;
|
||||
import com.sf.oauth.service.IAuthService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpSession;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
|
||||
import static org.apache.commons.lang3.StringUtils.EMPTY;
|
||||
|
||||
/**
|
||||
* @author zoukun
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/oauth")
|
||||
public class RestAuthController {
|
||||
|
||||
@Autowired
|
||||
private IAuthService authService;
|
||||
|
||||
@GetMapping("/authorize/{source}")
|
||||
public void authorize(@PathVariable("source") String source, HttpServletResponse response) throws IOException {
|
||||
log.info("进入render:" + source);
|
||||
AuthRequest authRequest = getAuthRequest(source, EMPTY, EMPTY);
|
||||
String authorizeUrl = authRequest.authorize(UUID.fastUUID().toString());
|
||||
log.info(authorizeUrl);
|
||||
response.sendRedirect(authorizeUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* oauth平台中配置的授权回调地址
|
||||
* 如在创建华为授权应用时的回调地址为:http://127.0.0.1:8080/oauth/callback/huawei
|
||||
*/
|
||||
@RequestMapping("/callback/{source}")
|
||||
public AjaxResult callback(@PathVariable("source") String source, @RequestBody AuthCallback callback, HttpSession session) {
|
||||
log.info("进入callback:" + source + " callback params:" + JSONObject.toJSONString(callback));
|
||||
AuthRequest authRequest = getAuthRequest(source, callback.getClientId(), callback.getClientSecret());
|
||||
AuthUser authUser = authRequest.callback(callback);
|
||||
log.info(JSONObject.toJSONString(authUser));
|
||||
AjaxResult ajax = AjaxResult.success();
|
||||
String token = authService.authLogin(authUser,session);
|
||||
ajax.put(Constants.TOKEN, token);
|
||||
return ajax;
|
||||
}
|
||||
|
||||
/*
|
||||
@RequestMapping("/revoke/{source}/{userId}")
|
||||
@ResponseBody
|
||||
public AjaxResult revokeAuth(@PathVariable("source") String source, @PathVariable("userId") String userId) throws IOException {
|
||||
AuthRequest authRequest = getAuthRequest(source.toLowerCase());
|
||||
|
||||
AuthUser user = userService.getByUuid(uuid);
|
||||
if (null == user) {
|
||||
return Response.error("用户不存在");
|
||||
}
|
||||
AjaxResult<AuthToken> response = null;
|
||||
try {
|
||||
response = authRequest.revoke(user.getToken());
|
||||
if (response.ok()) {
|
||||
userService.remove(user.getUuid());
|
||||
return Response.success("用户 [" + user.getUsername() + "] 的 授权状态 已收回!");
|
||||
}
|
||||
return Response.error("用户 [" + user.getUsername() + "] 的 授权状态 收回失败!" + response.getMsg());
|
||||
} catch (AuthException e) {
|
||||
return Response.error(e.getErrorMsg());
|
||||
}
|
||||
}
|
||||
|
||||
@RequestMapping("/refresh/{source}/{userId}")
|
||||
@ResponseBody
|
||||
public Object refreshAuth(@PathVariable("source") String source, @PathVariable("userId") String userId) {
|
||||
AuthRequest authRequest = getAuthRequest(source.toLowerCase());
|
||||
|
||||
AuthUser user = userService.getByUuid(userId);
|
||||
if (null == user) {
|
||||
return Response.error("用户不存在");
|
||||
}
|
||||
AjaxResult<AuthToken> response = null;
|
||||
try {
|
||||
response = authRequest.refresh(user.getToken());
|
||||
if (response.ok()) {
|
||||
user.setToken(response.getData());
|
||||
userService.save(user);
|
||||
return Response.success("用户 [" + user.getUsername() + "] 的 access token 已刷新!新的 callback: " + response.getData().getAccessToken());
|
||||
}
|
||||
return Response.error("用户 [" + user.getUsername() + "] 的 access token 刷新失败!" + response.getMsg());
|
||||
} catch (AuthException e) {
|
||||
return Response.error(e.getErrorMsg());
|
||||
}
|
||||
}*/
|
||||
|
||||
/**
|
||||
* 根据具体的授权来源,获取授权请求工具类
|
||||
*
|
||||
* @param source
|
||||
* @return
|
||||
*/
|
||||
private AuthRequest getAuthRequest(String source, String clientId, String clientSecret) {
|
||||
AuthRequest authRequest = null;
|
||||
switch (source.toLowerCase()) {
|
||||
case "huawei":
|
||||
authRequest = new AuthHuaweiRequest(AuthConfig.builder()
|
||||
.clientId(StrUtil.isBlank(clientId) ? "110693217" : clientId)
|
||||
.clientSecret(StrUtil.isBlank(clientSecret) ? "1410c01bc71c7ba587175ae79e500137c70945acc1416a38127cf98a09a6f8ba" : clientSecret)
|
||||
.redirectUri("")
|
||||
.ignoreCheckState(true)
|
||||
.scopes(Arrays.asList(
|
||||
AuthHuaweiScope.BASE_PROFILE.getScope(),
|
||||
AuthHuaweiScope.MOBILE_NUMBER.getScope(),
|
||||
AuthHuaweiScope.ACCOUNTLIST.getScope(),
|
||||
AuthHuaweiScope.SCOPE_DRIVE_FILE.getScope(),
|
||||
AuthHuaweiScope.SCOPE_DRIVE_APPDATA.getScope()
|
||||
))
|
||||
.build());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (null == authRequest) {
|
||||
throw new AuthException(HttpStatus.BAD_REQUEST, "未获取到有效的Auth配置");
|
||||
}
|
||||
return authRequest;
|
||||
}
|
||||
}
|
42
sf-oauth/src/main/java/com/sf/oauth/domain/AuthCallback.java
Normal file
42
sf-oauth/src/main/java/com/sf/oauth/domain/AuthCallback.java
Normal file
@ -0,0 +1,42 @@
|
||||
package com.sf.oauth.domain;
|
||||
|
||||
import lombok.*;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 授权回调时的参数类
|
||||
*
|
||||
* @author zk
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class AuthCallback implements Serializable {
|
||||
|
||||
/**
|
||||
* 访问AuthorizeUrl后回调时带的参数code
|
||||
*/
|
||||
private String code;
|
||||
|
||||
/**
|
||||
* 访问AuthorizeUrl后回调时带的参数state,用于和请求AuthorizeUrl前的state比较,防止CSRF攻击
|
||||
*/
|
||||
private String state;
|
||||
|
||||
/**
|
||||
* 华为授权登录
|
||||
*
|
||||
* 客户端id:对应各平台的appKey
|
||||
*/
|
||||
private String clientId;
|
||||
|
||||
/**
|
||||
* 客户端Secret:对应各平台的appSecret
|
||||
*/
|
||||
private String clientSecret;
|
||||
|
||||
|
||||
}
|
42
sf-oauth/src/main/java/com/sf/oauth/domain/AuthToken.java
Normal file
42
sf-oauth/src/main/java/com/sf/oauth/domain/AuthToken.java
Normal file
@ -0,0 +1,42 @@
|
||||
package com.sf.oauth.domain;
|
||||
|
||||
import lombok.*;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 授权所需的token
|
||||
*
|
||||
* @author zoukun
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class AuthToken implements Serializable {
|
||||
private String accessToken;
|
||||
private int expireIn;
|
||||
private String refreshToken;
|
||||
private int refreshTokenExpireIn;
|
||||
private String uid;
|
||||
private String openId;
|
||||
private String accessCode;
|
||||
private String unionId;
|
||||
|
||||
/**
|
||||
* 华为返回 生成的Access Token中包含的scope。
|
||||
*/
|
||||
private String scope;
|
||||
|
||||
/**
|
||||
* 华为返回 固定返回Bearer,标识返回Access Token的类型
|
||||
*/
|
||||
private String tokenType;
|
||||
|
||||
/**
|
||||
* 华为返回 返回JWT格式数据,包含用户基本帐号、用户邮箱等信息。
|
||||
* 参照https://developer.huawei.com/consumer/cn/doc/HMSCore-References/account-verify-id-token_hms_reference-0000001050050577#section3142132691914
|
||||
*/
|
||||
private String idToken;
|
||||
}
|
79
sf-oauth/src/main/java/com/sf/oauth/domain/AuthUser.java
Normal file
79
sf-oauth/src/main/java/com/sf/oauth/domain/AuthUser.java
Normal file
@ -0,0 +1,79 @@
|
||||
package com.sf.oauth.domain;
|
||||
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.sf.oauth.enums.AuthUserGender;
|
||||
import lombok.*;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 授权成功后的用户信息,根据授权平台的不同,获取的数据完整性也不同
|
||||
*
|
||||
* @author zoukun
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class AuthUser implements Serializable {
|
||||
/**
|
||||
* 用户第三方系统的唯一id。
|
||||
*/
|
||||
private String uuid;
|
||||
/**
|
||||
* 用户名
|
||||
*/
|
||||
private String username;
|
||||
/**
|
||||
* 用户昵称
|
||||
*/
|
||||
private String nickname;
|
||||
/**
|
||||
* 用户头像
|
||||
*/
|
||||
private String avatar;
|
||||
/**
|
||||
* 用户网址
|
||||
*/
|
||||
private String blog;
|
||||
/**
|
||||
* 所在公司
|
||||
*/
|
||||
private String company;
|
||||
/**
|
||||
* 位置
|
||||
*/
|
||||
private String location;
|
||||
/**
|
||||
* 用户邮箱
|
||||
*/
|
||||
private String email;
|
||||
/**
|
||||
* 用户手机号
|
||||
*/
|
||||
private String mobileNumber;
|
||||
/**
|
||||
* 用户备注(各平台中的用户个人介绍)
|
||||
*/
|
||||
private String remark;
|
||||
/**
|
||||
* 性别
|
||||
*/
|
||||
private AuthUserGender gender;
|
||||
/**
|
||||
* 用户来源
|
||||
*/
|
||||
private String source;
|
||||
/**
|
||||
* 用户授权的token信息
|
||||
*/
|
||||
private AuthToken token;
|
||||
/**
|
||||
* 第三方平台返回的原始用户信息
|
||||
*/
|
||||
private JSONObject rawUserInfo;
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
package com.sf.oauth.enums;
|
||||
|
||||
import com.sf.oauth.config.AuthSource;
|
||||
import com.sf.oauth.request.AuthDefaultRequest;
|
||||
import com.sf.oauth.request.AuthHuaweiRequest;
|
||||
|
||||
/**
|
||||
* 内置的各api需要的url, 用枚举类分平台类型管理
|
||||
*
|
||||
* @author zoukun
|
||||
*/
|
||||
public enum AuthDefaultSource implements AuthSource {
|
||||
|
||||
|
||||
/**
|
||||
* 华为oauth2/v3
|
||||
*/
|
||||
HUAWEI {
|
||||
@Override
|
||||
public String authorize() {
|
||||
return "https://oauth-login.cloud.huawei.com/oauth2/v3/authorize";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String accessToken() {
|
||||
return "https://oauth-login.cloud.huawei.com/oauth2/v3/token";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String userInfo() {
|
||||
return "https://account.cloud.huawei.com/rest.php";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String refresh() {
|
||||
return "https://oauth-login.cloud.huawei.com/oauth2/v3/token";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends AuthDefaultRequest> getTargetClass() {
|
||||
return AuthHuaweiRequest.class;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
package com.sf.oauth.enums;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* @author zoukun
|
||||
*/
|
||||
public enum AuthPlatformInfo {
|
||||
|
||||
/**
|
||||
* 平台
|
||||
*/
|
||||
HUAWEI("华为", "huawei", "https://developer.huawei.com/consumer/cn/doc/HMSCore-Guides/open-platform-oauth-0000001053629189"),
|
||||
;
|
||||
|
||||
/**
|
||||
* 平台名
|
||||
*/
|
||||
private final String name;
|
||||
/**
|
||||
* 平台编码
|
||||
*/
|
||||
private final String code;
|
||||
/**
|
||||
* 官网api文档
|
||||
*/
|
||||
private final String apiDoc;
|
||||
|
||||
AuthPlatformInfo(String name, String code, String apiDoc) {
|
||||
this.name = name;
|
||||
this.code = code;
|
||||
this.apiDoc = apiDoc;
|
||||
}
|
||||
|
||||
public static List<AuthPlatformInfo> getPlatformInfos() {
|
||||
return Arrays.asList(AuthPlatformInfo.values());
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public String getApiDoc() {
|
||||
return apiDoc;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
package com.sf.oauth.enums;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* 用户性别
|
||||
*
|
||||
* @author zoukun
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum AuthUserGender {
|
||||
/**
|
||||
* MALE/FAMALE为正常值,通过{@link AuthUserGender#getRealGender(String)}方法获取真实的性别
|
||||
* UNKNOWN为容错值,部分平台不会返回用户性别,为了方便统一,使用UNKNOWN标记所有未知或不可测的用户性别信息
|
||||
*/
|
||||
MALE("0", "男"),
|
||||
FEMALE("1", "女"),
|
||||
UNKNOWN("2", "未知");
|
||||
|
||||
private String code;
|
||||
private String desc;
|
||||
|
||||
/**
|
||||
* 获取用户的实际性别,常规网站
|
||||
*
|
||||
* @param originalGender 用户第三方标注的原始性别
|
||||
* @return 用户性别
|
||||
*/
|
||||
public static AuthUserGender getRealGender(String originalGender) {
|
||||
if (null == originalGender || UNKNOWN.getCode().equals(originalGender)) {
|
||||
return UNKNOWN;
|
||||
}
|
||||
String[] males = {"m", "男", "0", "male"};
|
||||
if (Arrays.asList(males).contains(originalGender.toLowerCase())) {
|
||||
return MALE;
|
||||
}
|
||||
return FEMALE;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
package com.sf.oauth.enums.scope;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 华为平台 OAuth 授权范围
|
||||
*
|
||||
* @author zoukun
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum AuthHuaweiScope implements AuthScope {
|
||||
|
||||
/**
|
||||
* {@code scope} 含义,以{@code description} 为准
|
||||
*/
|
||||
BASE_PROFILE("https://www.huawei.com/auth/account/base.profile", "获取用户的基本信息", true),
|
||||
MOBILE_NUMBER("https://www.huawei.com/auth/account/mobile.number", "获取用户的手机号", false),
|
||||
ACCOUNTLIST("https://www.huawei.com/auth/account/accountlist", "获取用户的账单列表", false),
|
||||
|
||||
/**
|
||||
* 以下两个 scope 不需要经过华为评估和验证
|
||||
*/
|
||||
SCOPE_DRIVE_FILE("https://www.huawei.com/auth/drive.file", "只允许访问由应用程序创建或打开的文件", false),
|
||||
SCOPE_DRIVE_APPDATA("https://www.huawei.com/auth/drive.appdata", "只允许访问由应用程序创建或打开的文件", false),
|
||||
/**
|
||||
* 以下四个 scope 使用前需要向drivekit@huawei.com提交申请
|
||||
* <p>
|
||||
* 参考:https://developer.huawei.com/consumer/cn/doc/development/HMSCore-Guides-V5/server-dev-0000001050039664-V5#ZH-CN_TOPIC_0000001050039664__section1618418855716
|
||||
*/
|
||||
SCOPE_DRIVE("https://www.huawei.com/auth/drive", "只允许访问由应用程序创建或打开的文件", false),
|
||||
SCOPE_DRIVE_READONLY("https://www.huawei.com/auth/drive.readonly", "只允许访问由应用程序创建或打开的文件", false),
|
||||
SCOPE_DRIVE_METADATA("https://www.huawei.com/auth/drive.metadata", "只允许访问由应用程序创建或打开的文件", false),
|
||||
SCOPE_DRIVE_METADATA_READONLY("https://www.huawei.com/auth/drive.metadata.readonly", "只允许访问由应用程序创建或打开的文件", false),
|
||||
|
||||
|
||||
;
|
||||
|
||||
private final String scope;
|
||||
private final String description;
|
||||
private final boolean isDefault;
|
||||
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
package com.sf.oauth.enums.scope;
|
||||
|
||||
/**
|
||||
* 各个平台 scope 类的统一接口
|
||||
*
|
||||
* @author zoukun
|
||||
*/
|
||||
public interface AuthScope {
|
||||
|
||||
/**
|
||||
* 获取字符串 {@code scope},对应为各平台实际使用的 {@code scope}
|
||||
*
|
||||
* @return String
|
||||
*/
|
||||
String getScope();
|
||||
|
||||
/**
|
||||
* 判断当前 {@code scope} 是否为各平台默认启用的
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
boolean isDefault();
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
package com.sf.oauth.exception;
|
||||
|
||||
|
||||
import com.sf.oauth.config.AuthSource;
|
||||
|
||||
/**
|
||||
* 授权异常
|
||||
*
|
||||
* @author zoukun
|
||||
*/
|
||||
public class AuthException extends RuntimeException {
|
||||
|
||||
private int errorCode;
|
||||
private String errorMsg;
|
||||
|
||||
public AuthException(String errorMsg) {
|
||||
super(errorMsg);
|
||||
this.errorMsg = errorMsg;
|
||||
}
|
||||
|
||||
public AuthException(int errorCode, String errorMsg) {
|
||||
super(errorMsg);
|
||||
this.errorCode = errorCode;
|
||||
this.errorMsg = errorMsg;
|
||||
}
|
||||
|
||||
public AuthException(int errorCode, String errorMsg, AuthSource source) {
|
||||
this(errorCode, String.format("%s [%s]", errorMsg, source.getName()));
|
||||
}
|
||||
|
||||
public AuthException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public AuthException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public int getErrorCode() {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
public String getErrorMsg() {
|
||||
return errorMsg;
|
||||
}
|
||||
}
|
@ -0,0 +1,177 @@
|
||||
package com.sf.oauth.request;
|
||||
|
||||
import cn.hutool.core.lang.UUID;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.core.util.URLUtil;
|
||||
import cn.hutool.http.HttpUtil;
|
||||
import com.sf.common.constant.HttpStatus;
|
||||
import com.sf.oauth.config.AuthConfig;
|
||||
import com.sf.oauth.config.AuthSource;
|
||||
import com.sf.oauth.exception.AuthException;
|
||||
import com.sf.oauth.domain.AuthCallback;
|
||||
import com.sf.oauth.domain.AuthToken;
|
||||
import com.sf.oauth.domain.AuthUser;
|
||||
import com.sf.oauth.utils.AuthChecker;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 默认的request处理类
|
||||
*
|
||||
* @author zoukun
|
||||
*/
|
||||
@Slf4j
|
||||
public abstract class AuthDefaultRequest implements AuthRequest {
|
||||
protected AuthConfig config;
|
||||
protected AuthSource source;
|
||||
|
||||
public AuthDefaultRequest(AuthConfig config, AuthSource source) {
|
||||
this.config = config;
|
||||
this.source = source;
|
||||
if (!AuthChecker.isSupportedAuth(config, source)) {
|
||||
throw new AuthException(HttpStatus.ERROR,"Parameter incomplete", source);
|
||||
}
|
||||
// 校验配置合法性
|
||||
// AuthChecker.checkConfig(config, source);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取access token
|
||||
*
|
||||
* @param authCallback 授权成功后的回调参数
|
||||
* @return token
|
||||
* @see AuthDefaultRequest#authorize()
|
||||
* @see AuthDefaultRequest#authorize(String)
|
||||
*/
|
||||
protected abstract AuthToken getAccessToken(AuthCallback authCallback);
|
||||
|
||||
/**
|
||||
* 使用token换取用户信息
|
||||
*
|
||||
* @param authToken token信息
|
||||
* @return 用户信息
|
||||
* @see AuthDefaultRequest#getAccessToken(AuthCallback)
|
||||
*/
|
||||
protected abstract AuthUser getUserInfo(AuthToken authToken);
|
||||
|
||||
/**
|
||||
* 统一的登录入口。当通过{@link AuthDefaultRequest#authorize(String)}授权成功后,会跳转到调用方的相关回调方法中
|
||||
* 方法的入参可以使用{@code AuthCallback},{@code AuthCallback}类中封装好了OAuth2授权回调所需要的参数
|
||||
*
|
||||
* @param authCallback 用于接收回调参数的实体
|
||||
* @return AuthResponse
|
||||
*/
|
||||
@Override
|
||||
public AuthUser callback(AuthCallback authCallback) {
|
||||
try {
|
||||
checkCode(authCallback);
|
||||
if (!config.isIgnoreCheckState()) {
|
||||
AuthChecker.checkState(authCallback.getState(), source);
|
||||
}
|
||||
|
||||
AuthToken authToken = this.getAccessToken(authCallback);
|
||||
return this.getUserInfo(authToken);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to callback with oauth authorization", e);
|
||||
throw new AuthException(HttpStatus.UNAUTHORIZED,"Failed to callback with oauth authorization");
|
||||
}
|
||||
}
|
||||
|
||||
protected void checkCode(AuthCallback authCallback) {
|
||||
AuthChecker.checkCode(source, authCallback);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 返回授权url,可自行跳转页面
|
||||
* <p>
|
||||
* 不建议使用该方式获取授权地址,不带{@code state}的授权地址,容易受到csrf攻击。
|
||||
* 建议使用{@link AuthDefaultRequest#authorize(String)}方法生成授权地址,在回调方法中对{@code state}进行校验
|
||||
*
|
||||
* @return 返回授权地址
|
||||
* @see AuthDefaultRequest#authorize(String)
|
||||
*/
|
||||
@Override
|
||||
public String authorize() {
|
||||
return this.authorize(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回带{@code state}参数的授权url,授权回调时会带上这个{@code state}
|
||||
*
|
||||
* @param state state 验证授权流程的参数,可以防止csrf
|
||||
* @return 返回授权地址
|
||||
*/
|
||||
@Override
|
||||
public String authorize(String state) {
|
||||
Map<String, Object> form = new HashMap<>(8);
|
||||
form.put("client_id", config.getClientId());
|
||||
form.put("client_secret", config.getClientSecret());
|
||||
form.put("redirect_uri", config.getRedirectUri());
|
||||
form.put("state", getRealState(state));
|
||||
form.put("response_type","code");
|
||||
return HttpUtil.get(source.authorize(), form);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回获取accessToken的url
|
||||
*
|
||||
* @param code 授权码
|
||||
* @return 返回获取accessToken的url
|
||||
*/
|
||||
protected String accessTokenUrl(String code) {
|
||||
Map<String, Object> form = new HashMap<>(8);
|
||||
form.put("code", code);
|
||||
form.put("client_id", config.getClientId());
|
||||
form.put("client_secret", config.getClientSecret());
|
||||
form.put("grant_type", "authorization_code");
|
||||
form.put("redirect_uri", config.getRedirectUri());
|
||||
return HttpUtil.get(source.accessToken(),form);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 获取state,如果为空, 则默认取当前日期的时间戳
|
||||
*
|
||||
* @param state 原始的state
|
||||
* @return 返回不为null的state
|
||||
*/
|
||||
protected String getRealState(String state) {
|
||||
if (StrUtil.isEmpty(state)) {
|
||||
state = UUID.fastUUID().toString();
|
||||
}
|
||||
// todo 需要缓存state
|
||||
return state;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取以 {@code separator}分割过后的 scope 信息
|
||||
*
|
||||
* @param separator 多个 {@code scope} 间的分隔符
|
||||
* @param encode 是否 encode 编码
|
||||
* @param defaultScopes 默认的 scope, 当客户端没有配置 {@code scopes} 时启用
|
||||
* @return String
|
||||
*/
|
||||
protected String getScopes(String separator, boolean encode, List<String> defaultScopes) {
|
||||
List<String> scopes = config.getScopes();
|
||||
if (null == scopes || scopes.isEmpty()) {
|
||||
if (null == defaultScopes || defaultScopes.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
scopes = defaultScopes;
|
||||
}
|
||||
if (null == separator) {
|
||||
// 默认为空格
|
||||
separator = " ";
|
||||
}
|
||||
String scopeStr = String.join(separator, scopes);
|
||||
return encode ? URLUtil.encode(scopeStr) : scopeStr;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,168 @@
|
||||
package com.sf.oauth.request;
|
||||
|
||||
|
||||
import cn.hutool.http.HttpUtil;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.sf.common.constant.HttpStatus;
|
||||
import com.sf.common.core.domain.AjaxResult;
|
||||
import com.sf.oauth.config.AuthConfig;
|
||||
import com.sf.oauth.enums.AuthDefaultSource;
|
||||
import com.sf.oauth.enums.AuthUserGender;
|
||||
import com.sf.oauth.enums.scope.AuthHuaweiScope;
|
||||
import com.sf.oauth.exception.AuthException;
|
||||
import com.sf.oauth.domain.AuthCallback;
|
||||
import com.sf.oauth.domain.AuthToken;
|
||||
import com.sf.oauth.domain.AuthUser;
|
||||
import com.sf.oauth.utils.AuthScopeUtils;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
/**
|
||||
* 华为授权登录
|
||||
*
|
||||
* @author zkun
|
||||
*/
|
||||
@Slf4j
|
||||
public class AuthHuaweiRequest extends AuthDefaultRequest {
|
||||
|
||||
public AuthHuaweiRequest(AuthConfig config) {
|
||||
super(config, AuthDefaultSource.HUAWEI);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取access token
|
||||
*
|
||||
* @param authCallback 授权成功后的回调参数
|
||||
* @return token
|
||||
* @see AuthDefaultRequest#authorize()
|
||||
* @see AuthDefaultRequest#authorize(String)
|
||||
*/
|
||||
@Override
|
||||
protected AuthToken getAccessToken(AuthCallback authCallback) {
|
||||
Map<String, Object> form = new HashMap<>(8);
|
||||
form.put("grant_type", "authorization_code");
|
||||
form.put("code", authCallback.getCode());
|
||||
form.put("client_id", config.getClientId());
|
||||
form.put("client_secret", config.getClientSecret());
|
||||
form.put("redirect_uri", config.getRedirectUri());
|
||||
String response = HttpUtil.post(source.accessToken(), form);
|
||||
return getAuthToken(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用token换取用户信息
|
||||
*
|
||||
* @param authToken token信息
|
||||
* @return 用户信息
|
||||
* @see AuthDefaultRequest#getAccessToken(AuthCallback)
|
||||
*/
|
||||
@Override
|
||||
protected AuthUser getUserInfo(AuthToken authToken) {
|
||||
Map<String, Object> form = new HashMap<>(8);
|
||||
form.put("nsp_ts", System.currentTimeMillis() + "");
|
||||
form.put("access_token", authToken.getAccessToken());
|
||||
form.put("nsp_svc", "GOpen.User.getInfo");
|
||||
form.put("getNickName", "1");
|
||||
String response = HttpUtil.post(source.userInfo(), form);
|
||||
log.info(response);
|
||||
|
||||
JSONObject object = JSONObject.parseObject(response);
|
||||
this.checkResponse(object);
|
||||
AuthUserGender gender = getRealGender(object);
|
||||
return AuthUser.builder()
|
||||
.rawUserInfo(object)
|
||||
.uuid(object.getString("unionID"))
|
||||
.username(object.getString("displayName"))
|
||||
.nickname(object.getString("displayName"))
|
||||
.gender(gender)
|
||||
.avatar(object.getString("headPictureURL"))
|
||||
.mobileNumber(object.getString("mobileNumber"))
|
||||
.email(object.getString("email"))
|
||||
.token(authToken)
|
||||
.source(source.toString())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新access token (续期)
|
||||
*
|
||||
* @param authToken 登录成功后返回的Token信息
|
||||
* @return AuthResponse
|
||||
*/
|
||||
@Override
|
||||
public AjaxResult refresh(AuthToken authToken) {
|
||||
Map<String, Object> form = new HashMap<>(8);
|
||||
form.put("client_id", config.getClientId());
|
||||
form.put("client_secret", config.getClientSecret());
|
||||
form.put("refresh_token", authToken.getRefreshToken());
|
||||
form.put("grant_type", "refresh_token");
|
||||
String response = HttpUtil.post(source.refresh(), form);
|
||||
return AjaxResult.success(getAuthToken(response));
|
||||
}
|
||||
|
||||
private AuthToken getAuthToken(String response) {
|
||||
JSONObject object = JSONObject.parseObject(response);
|
||||
|
||||
this.checkResponse(object);
|
||||
|
||||
return AuthToken.builder()
|
||||
.accessToken(object.getString("access_token"))
|
||||
.expireIn(object.getIntValue("expires_in"))
|
||||
.refreshToken(object.getString("refresh_token"))
|
||||
.scope(object.getString("scope"))
|
||||
.tokenType(object.getString("token_type"))
|
||||
.idToken(object.getString("id_token"))
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回带{@code state}参数的授权url,授权回调时会带上这个{@code state}
|
||||
*
|
||||
* @param state state 验证授权流程的参数,可以防止csrf
|
||||
* @return 返回授权地址
|
||||
*/
|
||||
@Override
|
||||
public String authorize(String state) {
|
||||
Map<String, Object> form = new HashMap<>(8);
|
||||
form.put("client_id", config.getClientId());
|
||||
form.put("client_secret", config.getClientSecret());
|
||||
form.put("redirect_uri", config.getRedirectUri());
|
||||
form.put("state", getRealState(state));
|
||||
form.put("response_type", "code");
|
||||
form.put("access_type", "offline");
|
||||
form.put("scope", this.getScopes(" ", true, AuthScopeUtils.getDefaultScopes(AuthHuaweiScope.values())));
|
||||
return HttpUtil.get(source.authorize(), form);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取用户的实际性别。华为系统中,用户的性别:1表示女,0表示男
|
||||
*
|
||||
* @param object obj
|
||||
* @return AuthUserGender
|
||||
*/
|
||||
private AuthUserGender getRealGender(JSONObject object) {
|
||||
int genderCodeInt = object.getIntValue("gender");
|
||||
String genderCode = genderCodeInt == 0 ? "0" : (genderCodeInt == 1) ? "1" : genderCodeInt + "";
|
||||
return AuthUserGender.getRealGender(genderCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验响应结果
|
||||
*
|
||||
* @param object 接口返回的结果
|
||||
*/
|
||||
private void checkResponse(JSONObject object) {
|
||||
if (object.containsKey("NSP_STATUS")) {
|
||||
throw new AuthException(object.getString("error"));
|
||||
}
|
||||
if (object.containsKey("error")) {
|
||||
throw new AuthException(object.getString("sub_error") + ":" + object.getString("error_description"));
|
||||
}
|
||||
}
|
||||
}
|
76
sf-oauth/src/main/java/com/sf/oauth/request/AuthRequest.java
Normal file
76
sf-oauth/src/main/java/com/sf/oauth/request/AuthRequest.java
Normal file
@ -0,0 +1,76 @@
|
||||
package com.sf.oauth.request;
|
||||
|
||||
|
||||
import com.sf.common.constant.HttpStatus;
|
||||
import com.sf.common.core.domain.AjaxResult;
|
||||
import com.sf.oauth.domain.AuthUser;
|
||||
import com.sf.oauth.exception.AuthException;
|
||||
import com.sf.oauth.domain.AuthCallback;
|
||||
import com.sf.oauth.domain.AuthToken;
|
||||
|
||||
/**
|
||||
* {@code Request}公共接口,所有平台的{@code Request}都需要实现该接口
|
||||
* <p>
|
||||
* {@link AuthRequest#authorize()}
|
||||
* {@link AuthRequest#authorize(String)}
|
||||
* {@link AuthRequest#callback(AuthCallback)}
|
||||
* {@link AuthRequest#revoke(AuthToken)}
|
||||
* {@link AuthRequest#refresh(AuthToken)}
|
||||
*
|
||||
* @author zoukun
|
||||
*/
|
||||
public interface AuthRequest {
|
||||
|
||||
/**
|
||||
* 返回授权url,可自行跳转页面
|
||||
* <p>
|
||||
* 不建议使用该方式获取授权地址,不带{@code state}的授权地址,容易受到csrf攻击。
|
||||
* 建议使用{@link AuthDefaultRequest#authorize(String)}方法生成授权地址,在回调方法中对{@code state}进行校验
|
||||
*
|
||||
* @return 返回授权地址
|
||||
*/
|
||||
@Deprecated
|
||||
default String authorize() {
|
||||
throw new AuthException(HttpStatus.UNAUTHORIZED,"Not Implemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回带{@code state}参数的授权url,授权回调时会带上这个{@code state}
|
||||
*
|
||||
* @param state state 验证授权流程的参数,可以防止csrf
|
||||
* @return 返回授权地址
|
||||
*/
|
||||
default String authorize(String state) {
|
||||
throw new AuthException(HttpStatus.UNAUTHORIZED,"Not Implemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* 第三方登录
|
||||
*
|
||||
* @param authCallback 用于接收回调参数的实体
|
||||
* @return 返回登录成功后的用户信息
|
||||
*/
|
||||
default AuthUser callback(AuthCallback authCallback) {
|
||||
throw new AuthException(HttpStatus.UNAUTHORIZED,"Not Implemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* 撤销授权
|
||||
*
|
||||
* @param authToken 登录成功后返回的Token信息
|
||||
* @return AjaxResult
|
||||
*/
|
||||
default AjaxResult revoke(AuthToken authToken) {
|
||||
throw new AuthException(HttpStatus.UNAUTHORIZED,"Not Implemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新access token (续期)
|
||||
*
|
||||
* @param authToken 登录成功后返回的Token信息
|
||||
* @return AjaxResult
|
||||
*/
|
||||
default AjaxResult refresh(AuthToken authToken) {
|
||||
throw new AuthException(HttpStatus.UNAUTHORIZED,"Not Implemented");
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
package com.sf.oauth.service;
|
||||
|
||||
import com.sf.oauth.domain.AuthUser;
|
||||
|
||||
import javax.servlet.http.HttpSession;
|
||||
|
||||
/**
|
||||
* 功能描述:
|
||||
*
|
||||
* @author a_kun
|
||||
* @date 2024/4/12 10:21
|
||||
*/
|
||||
public interface IAuthService {
|
||||
String authLogin(AuthUser authUser, HttpSession session);
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
package com.sf.oauth.service.impl;
|
||||
|
||||
import com.sf.common.core.domain.AjaxResult;
|
||||
import com.sf.common.core.domain.entity.SysUser;
|
||||
import com.sf.common.enums.UserStatus;
|
||||
import com.sf.common.utils.SecurityUtils;
|
||||
import com.sf.common.utils.ip.IpUtils;
|
||||
import com.sf.framework.web.service.SysLoginService;
|
||||
import com.sf.oauth.domain.AuthUser;
|
||||
import com.sf.oauth.exception.AuthException;
|
||||
import com.sf.oauth.service.IAuthService;
|
||||
import com.sf.system.service.ISysUserService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.servlet.http.HttpSession;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 功能描述:
|
||||
*
|
||||
* @author a_kun
|
||||
* @date 2024/4/12 10:21
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class IAuthServiceImpl implements IAuthService {
|
||||
|
||||
@Autowired
|
||||
private SysLoginService loginService;
|
||||
|
||||
@Autowired
|
||||
private ISysUserService userService;
|
||||
|
||||
|
||||
@Override
|
||||
public String authLogin(AuthUser authUser, HttpSession session) {
|
||||
// TODO 应该要登录后绑定一个平台账号,没有平台账号要创建,保存用户的三方来源,及用户的三方唯一标识和绑定的用户
|
||||
if (authUser != null) {
|
||||
SysUser sysUser = new SysUser();
|
||||
sysUser.setUserName(authUser.getUsername());
|
||||
if (userService.checkUserNameUnique(sysUser)) {
|
||||
// 注册用户后登录
|
||||
sysUser.setNickName(authUser.getNickname());
|
||||
sysUser.setEmail(authUser.getEmail());
|
||||
sysUser.setPhonenumber(authUser.getMobileNumber());
|
||||
sysUser.setSex(authUser.getGender().getCode());
|
||||
sysUser.setAvatar(authUser.getAvatar());
|
||||
sysUser.setPassword(SecurityUtils.encryptPassword("ztzh@sac123"));
|
||||
sysUser.setStatus(UserStatus.OK.getCode());
|
||||
sysUser.setDelFlag("0");
|
||||
sysUser.setLoginIp(IpUtils.getHostIp());
|
||||
sysUser.setLoginDate(new Date());
|
||||
userService.insertUser(sysUser);
|
||||
}
|
||||
// 生成令牌
|
||||
return loginService.noPwdLogin(authUser.getUsername(), session);
|
||||
}
|
||||
throw new AuthException("login error");
|
||||
}
|
||||
}
|
79
sf-oauth/src/main/java/com/sf/oauth/utils/AuthChecker.java
Normal file
79
sf-oauth/src/main/java/com/sf/oauth/utils/AuthChecker.java
Normal file
@ -0,0 +1,79 @@
|
||||
package com.sf.oauth.utils;
|
||||
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.sf.common.constant.HttpStatus;
|
||||
import com.sf.oauth.config.AuthConfig;
|
||||
import com.sf.oauth.enums.AuthDefaultSource;
|
||||
import com.sf.oauth.config.AuthSource;
|
||||
import com.sf.oauth.exception.AuthException;
|
||||
import com.sf.oauth.domain.AuthCallback;
|
||||
|
||||
/**
|
||||
* 授权配置类的校验器
|
||||
*
|
||||
* @author ZK
|
||||
*/
|
||||
public class AuthChecker {
|
||||
|
||||
/**
|
||||
* 是否支持第三方登录
|
||||
*
|
||||
* @param config config
|
||||
* @param source source
|
||||
* @return true or false
|
||||
*/
|
||||
public static boolean isSupportedAuth(AuthConfig config, AuthSource source) {
|
||||
|
||||
return StrUtil.isNotEmpty(config.getClientId())
|
||||
&& StrUtil.isNotEmpty(config.getClientSecret())
|
||||
&& null != source;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查配置合法性。针对部分平台, 对redirect uri有特定要求。一般来说redirect uri都是http://,
|
||||
* 而对于部分平台, redirect uri 必须是https的链接
|
||||
*
|
||||
* @param config config
|
||||
* @param source source
|
||||
*/
|
||||
public static void checkConfig(AuthConfig config, AuthSource source) {
|
||||
String redirectUri = config.getRedirectUri();
|
||||
if (StrUtil.isEmpty(redirectUri)) {
|
||||
throw new AuthException(HttpStatus.BAD_REQUEST, "Illegal redirect uri", source);
|
||||
}
|
||||
if (!AuthUtils.isHttpProtocol(redirectUri) && !AuthUtils.isHttpsProtocol(redirectUri)) {
|
||||
throw new AuthException(HttpStatus.BAD_REQUEST, "Illegal redirect uri", source);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验回调传回的code
|
||||
* <p>
|
||||
* {@code v1.10.0}版本中改为传入{@code source}和{@code callback},对于不同平台使用不同参数接受code的情况统一做处理
|
||||
*
|
||||
* @param source 当前授权平台
|
||||
* @param callback 从第三方授权回调回来时传入的参数集合
|
||||
*/
|
||||
public static void checkCode(AuthSource source, AuthCallback callback) {
|
||||
if (StrUtil.isEmpty(callback.getCode())) {
|
||||
throw new AuthException(HttpStatus.UNAUTHORIZED,"Illegal code", source);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验回调传回的{@code state},为空或者不存在
|
||||
* <p>
|
||||
* {@code state}不存在的情况只有两种:
|
||||
* 1. {@code state}已使用,被正常清除
|
||||
* 2. {@code state}为前端伪造,本身就不存在
|
||||
*
|
||||
* @param state {@code state}一定不为空
|
||||
* @param source {@code source}当前授权平台
|
||||
*/
|
||||
public static void checkState(String state, AuthSource source) {
|
||||
if (StrUtil.isEmpty(state)) {
|
||||
throw new AuthException(HttpStatus.UNAUTHORIZED,"Illegal state", source);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
package com.sf.oauth.utils;
|
||||
|
||||
import com.sf.oauth.enums.scope.AuthScope;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Scope 工具类,提供对 scope 类的统一操作
|
||||
*
|
||||
* @author zoukun
|
||||
*/
|
||||
public class AuthScopeUtils {
|
||||
|
||||
/**
|
||||
* 获取 {@link com.sf.oauth.enums.scope.AuthScope} 数组中所有的被标记为 {@code default} 的 scope
|
||||
*
|
||||
* @param scopes scopes
|
||||
* @return List
|
||||
*/
|
||||
public static List<String> getDefaultScopes(AuthScope[] scopes) {
|
||||
if (null == scopes || scopes.length == 0) {
|
||||
return null;
|
||||
}
|
||||
return Arrays.stream(scopes)
|
||||
.filter((AuthScope::isDefault))
|
||||
.map(AuthScope::getScope)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 {@link com.sf.oauth.enums.scope.AuthScope} 数组中获取实际的 scope 字符串
|
||||
*
|
||||
* @param scopes 可变参数,支持传任意 {@link com.sf.oauth.enums.scope.AuthScope}
|
||||
* @return List
|
||||
*/
|
||||
public static List<String> getScopes(AuthScope... scopes) {
|
||||
if (null == scopes || scopes.length == 0) {
|
||||
return null;
|
||||
}
|
||||
return Arrays.stream(scopes).map(AuthScope::getScope).collect(Collectors.toList());
|
||||
}
|
||||
}
|
127
sf-oauth/src/main/java/com/sf/oauth/utils/AuthUtils.java
Normal file
127
sf-oauth/src/main/java/com/sf/oauth/utils/AuthUtils.java
Normal file
@ -0,0 +1,127 @@
|
||||
|
||||
package com.sf.oauth.utils;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.sf.oauth.exception.AuthException;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLDecoder;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class AuthUtils {
|
||||
private static final Charset DEFAULT_ENCODING;
|
||||
private static final String HMAC_SHA1 = "HmacSHA1";
|
||||
private static final String HMAC_SHA_256 = "HmacSHA256";
|
||||
|
||||
public AuthUtils() {
|
||||
}
|
||||
|
||||
public static String urlEncode(String value) {
|
||||
if (value == null) {
|
||||
return "";
|
||||
} else {
|
||||
try {
|
||||
String encoded = URLEncoder.encode(value, DEFAULT_ENCODING.displayName());
|
||||
return encoded.replace("+", "%20").replace("*", "%2A").replace("~", "%7E").replace("/", "%2F");
|
||||
} catch (UnsupportedEncodingException var2) {
|
||||
throw new AuthException("Failed To Encode Uri", var2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static String urlDecode(String value) {
|
||||
if (value == null) {
|
||||
return "";
|
||||
} else {
|
||||
try {
|
||||
return URLDecoder.decode(value, DEFAULT_ENCODING.displayName());
|
||||
} catch (UnsupportedEncodingException var2) {
|
||||
throw new AuthException("Failed To Decode Uri", var2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static String parseMapToString(Map<String, String> params, boolean encode) {
|
||||
if (null != params && !params.isEmpty()) {
|
||||
List<String> paramList = new ArrayList();
|
||||
params.forEach((k, v) -> {
|
||||
if (null == v) {
|
||||
paramList.add(k + "=");
|
||||
} else {
|
||||
paramList.add(k + "=" + (encode ? urlEncode(v) : v));
|
||||
}
|
||||
|
||||
});
|
||||
return String.join("&", paramList);
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isHttpProtocol(String url) {
|
||||
if (StrUtil.isEmpty(url)) {
|
||||
return false;
|
||||
} else {
|
||||
return url.startsWith("http://") || url.startsWith("http%3A%2F%2F");
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isHttpsProtocol(String url) {
|
||||
if (StrUtil.isEmpty(url)) {
|
||||
return false;
|
||||
} else {
|
||||
return url.startsWith("https://") || url.startsWith("https%3A%2F%2F");
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isLocalHost(String url) {
|
||||
return StrUtil.isEmpty(url) || url.contains("127.0.0.1") || url.contains("localhost");
|
||||
}
|
||||
|
||||
public static boolean isHttpsProtocolOrLocalHost(String url) {
|
||||
if (StrUtil.isEmpty(url)) {
|
||||
return false;
|
||||
} else {
|
||||
return isHttpsProtocol(url) || isLocalHost(url);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static String getTimestamp() {
|
||||
return String.valueOf(System.currentTimeMillis() / 1000L);
|
||||
}
|
||||
|
||||
|
||||
public static String md5(String str) {
|
||||
MessageDigest md = null;
|
||||
StringBuilder buffer = null;
|
||||
|
||||
try {
|
||||
md = MessageDigest.getInstance("MD5");
|
||||
md.update(str.getBytes(StandardCharsets.UTF_8));
|
||||
byte[] byteData = md.digest();
|
||||
buffer = new StringBuilder();
|
||||
byte[] var4 = byteData;
|
||||
int var5 = byteData.length;
|
||||
|
||||
for(int var6 = 0; var6 < var5; ++var6) {
|
||||
byte byteDatum = var4[var6];
|
||||
buffer.append(Integer.toString((byteDatum & 255) + 256, 16).substring(1));
|
||||
}
|
||||
} catch (Exception var8) {
|
||||
}
|
||||
|
||||
return null == buffer ? "" : buffer.toString();
|
||||
}
|
||||
|
||||
static {
|
||||
DEFAULT_ENCODING = StandardCharsets.UTF_8;
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user