概述
将 Just Auth Plus 以及 Just Auth Plus Identity Server 适配进 Solon,弥补了 Solon 缺少完整的认证体系的问题,为所有使用 Solon 进行快速开发的你们提供了更加方便的工具。
Just Auth Plus 方面目前只完成了 Simple、Socail 以及 Mfa 部分,如果你对 OICD、LADP、HTTP API 的适配有需求,请添加 QQ 群
22200020
,大鸽子浅念子会光速适配。
开始食用
你需要引入我们已经适配好的依赖来引入整个体系,当然如果你只使用其中一个也可以只引用其中一个:
<dependency>
<groupId>org.noear</groupId>
<artifactId>jap-solon-plugin</artifactId>
</dependency>
<dependency>
<groupId>org.noear</groupId>
<artifactId>jap-ids-solon-plugin</artifactId>
</dependency>
Maven Reload!
借这个机会我们来趁机配置一下这两个组件(在你的 app.yml 中):
jap:
# Auth 控制器注册根路径
authPath: /auth
# Account 控制器注册根路径
accountPath: /account
# 生成 Mfa QRCode 时 Issuer
issuer: SakuraImpression
# JapConfig 映射
japConfig:
# 是否启用单点登录
sso: true
# SSOConfig 映射
ssoConfig:
# 指定了 Cookie 使用的域名
cookieDomain: 127.0.0.1:6040
# SimpleConfig 映射
# !!! 指定该项启动 Simple !!!
simpleConfig:
# 指定了 remberMe 加密的 盐
credentialEncryptSalt: a1f735ed0cffd6f5ea80f8ee7ba68d02
# 社会化登录第三方整数列表
# 其中的每一项均为 AuthConfig 映射
# !!! 指定该项启动 Social !!!
credentials:
gitee:
clientId: b002b405304bd0b384029e8b04017349026bcca5cfe73cc6f5ca047cc4fe9241
clientSecret: 7942f86793e5fc1b73f8e3b2f2ee1925b0c9e923b0819a32097048bbeb15b5
redirectUri: http://127.0.0.1:6040/auth/social/gitee
github:
clientId: c394492091a984o659bc
clientSecret: 60c82d553f1b0dac17f2164eabc4ac63ffc831ca
redirectUri: http://127.0.0.1:6040/auth/social/github/callback
# 下一跳地址白名单,验证成功或失败后的跳转地址
# 更是确定是否为前后端分离的重要请求参数
nexts:
- https://passport.liusuyun.com/
- /auth/social/bind
- http://127.0.0.1:8000/auth/social#data={data}&code={code}
# Just Auth Plus Identity Server 配置
ids:
# 全部控制器注册的根路径,默认为 /oauth
basePath: /auth/o
# IdsConfig 映射
config:
# 服务根路径,用于 服务发现
issuer: http://127.0.0.1:6040
# Jwt 密钥配置
jwtConfig:
jwksKeyId: jap-jwk-keyid
jwksJson: |-
{
"keys": [
{
"p": "v5G0QPkr9zi1znff2g7p5K1ac1F2KNjXmk31Etl0UrRBwHiTxM_MkkldGlxnXWoFL4_cPZZMt_W14Td5qApknLFOh9iRWRPwqlFgC-eQzUjPeYvxjRbtV5QUHtbzrDCLjLiSNyhsLXHyi_yOawD2BS4U6sBWMSJlL2lShU7EAaU",
"kty": "RSA",
"q": "s2X9UeuEWky_io9hFAoHZjBxMBheNAGrHXtWat6zlg2tf_SIKpZ7Xs8C_-kr9Pvj-D428QsOjFZE-EtNBSXoMrvlMk7fGDl9x1dHvLS9GSitkXH2-Wthg8j0j0nfAmyEt94jP-XEkYic1Ok7EfBOPuvL21HO7YuB-cOff9ZGvBk",
"d": "Rj-QBeBdx85VIHkwVY1T94ZeeC_Z6Zw-cz5lk5Msw0U9QhSTWo28-d2lYjK7dhQn-E19JhTbCVE11UuUqENKZmO__yRgO1UJaj2x6vWMtgJptah7m8lI-QW0w6TnVxAHWfRPpKSEfbN4SpeufYf5PYhmmzT0A954Z2o0kqS4iHd0gwNAovOXaxriGXO1CcOQjBFEcm0BdboQZ7CKCoJ1D6S0xZpVFSJg-1AtagY5dzStyekzETO2tQSmVw4ogIoJsIbu3aYwbukmCoULQfJ36D0mPzrTG5oocEbbuCps_vH72VjZORHHAl4hwritFT_jD2bdQHSNMGukga8C0L1WQQ",
"e": "AQAB",
"use": "sig",
"kid": "jap-jwk-keyid",
"qi": "Asr5dZMDvwgquE6uFnDaBC76PY5JUzxQ5wY5oc4nhIm8UxQWwYZTWq-HOWkMB5c99fG1QxLWQKGtsguXfOXoNgnI--yHzLZcXf1XAd0siguaF1cgQIqwRUf4byofE6uJ-2ZON_ezn6Uvly8fDIlgwmKAiiwWvHI4iLqvqOReBgs",
"dp": "oIUzuFnR6FcBqJ8z2KE0haRorUZuLy38A1UdbQz_dqmKiv--OmUw8sc8l3EkP9ctvzvZfVWqtV7TZ4M3koIa6l18A0KKEE0wFVcYlwETiaBgEWYdIm86s27mKS1Og1MuK90gz800UCQx6_DVWX41qAOEDWzbDFLY3JBxUDi-7u0",
"alg": "RS256",
"dq": "MpNSM0IecgapCTsatzeMlnaZsmFsTWUbBJi86CwYnPkGLMiXisoZxcS-p77osYxB3L5NZu8jDtVTZFx2PjlNmN_34ZLyujWbDBPDGaQqm2koZZSnd_GZ8Dk7GRpOULSfRebOMTlpjU3iSPPnv0rsBDkdo5sQp09pOSy5TqTuFCE",
"n": "hj8zFdhYFi-47PO4B4HTRuOLPR_rpZJi66g4JoY4gyhb5v3Q57etSU9BnW9QQNoUMDvhCFSwkz0hgY5HqVj0zOG5s9x2a594UDIinKsm434b-pT6bueYdvM_mIUEKka5pqhy90wTTka42GvM-rBATHPTarq0kPTR1iBtYao8zX-RWmCbdumEWOkMFUGbBkUcOSJWzoLzN161WdYr2kJU5PFraUP3hG9fPpMEtvqd6IwEL-MOVx3nqc7zk3D91E6eU7EaOy8nz8echQLl6Ps34BSwEpgOhaHDD6IJzetW-KorYeC0r0okXhrl0sUVE2c71vKPVVtueJSIH6OwA3dVHQ"
}
]
}
文中所指的映射是指该条目中的配置项会使用
Snack3
通过反射的方式,注入生成相关 Bean 实例,所以该类下的所有字段你都可以自由配置。
相关服务实现与注入
以下服务的实现仅供参考,请根据具体业务逻辑来调整。
我们推荐封装一个公共 Service 用于用户查询,因为会反复使用相同代码。
package com.liusuyun.flowersay.gateway.services;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.liusuyun.flowersay.common.entities.models.User;
import com.liusuyun.flowersay.common.mappers.UserMapper;
import com.liusuyun.flowersay.common.utils.AbstractUtils;
import org.apache.ibatis.ext.solon.Db;
/**
* @author 颖
*/
public abstract class JapService extends AbstractUtils {
@Db
UserMapper userMapper;
protected User findUser(String username) {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("username", username)
.or().eq("email", username);
return this.userMapper.selectOne(queryWrapper);
}
protected User findUser(Long id) {
return this.userMapper.selectById(id);
}
}
package com.liusuyun.flowersay.gateway.services;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.fujieid.jap.ids.model.ClientDetail;
import com.fujieid.jap.ids.model.enums.GrantType;
import com.fujieid.jap.ids.model.enums.ResponseType;
import com.fujieid.jap.ids.service.IdsClientDetailService;
import com.liusuyun.flowersay.gateway.entities.OAuthClient;
import com.liusuyun.flowersay.gateway.mappers.OAuthClientMapper;
import org.apache.ibatis.ext.solon.Db;
import org.noear.solon.annotation.Component;
import org.noear.solon.annotation.Inject;
import java.util.List;
import java.util.stream.Collectors;
/**
* @author 颖
* @version 1.0.0
* @date 2021-04-14 10:27
* @since 1.0.0
*/
public class IdsClientDetailServiceImpl implements IdsClientDetailService {
@Db
OAuthClientMapper oAuthClientMapper;
/**
* 通过 client_id 查询客户端信息
*
* @param clientId 客户端应用id
* @return AppOauthClientDetails
*/
@Override
public ClientDetail getByClientId(String clientId) {
QueryWrapper<OAuthClient> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("client_id", clientId);
return this.convert(
this.oAuthClientMapper.selectOne(queryWrapper)
);
}
/**
* Add client
*
* @param clientDetail Client application details
* @return ClientDetail
*/
@Override
public ClientDetail add(ClientDetail clientDetail) {
return IdsClientDetailService.super.add(clientDetail);
}
/**
* Modify the client
*
* @param clientDetail Client application details
* @return ClientDetail
*/
@Override
public ClientDetail update(ClientDetail clientDetail) {
return IdsClientDetailService.super.update(clientDetail);
}
/**
* Delete client by primary key
*
* @param id Primary key of the client application
* @return boolean
*/
@Override
public boolean removeById(String id) {
this.oAuthClientMapper.deleteById(Long.parseLong(id));
return true;
}
/**
* Delete client by client id
*
* @param clientId Client application id
* @return ClientDetail
*/
@Override
public boolean removeByClientId(String clientId) {
QueryWrapper<OAuthClient> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("client_id", clientId);
this.oAuthClientMapper.delete(queryWrapper);
return true;
}
/**
* 获取所有 client detail
*
* @return List
*/
@Override
public List<ClientDetail> getAllClientDetail() {
return this.oAuthClientMapper.selectList(new QueryWrapper<>())
.stream()
.map(this::convert)
.collect(Collectors.toList());
}
private ClientDetail convert(OAuthClient client) {
if(client == null) {
return null;
}
return new ClientDetail()
.setId(String.valueOf(client.getId()))
.setAppName(client.getName())
.setClientId(client.getClientId())
.setClientSecret(client.getClientSecret())
.setSiteDomain(client.getSiteDomain())
.setRedirectUri(client.getRedirectUri())
.setLogoutRedirectUri(client.getLogoutUri())
.setLogo(client.getLogo())
.setAvailable(client.isAvailable())
.setDescription(client.getDescription())
.setScopes(client.getScopes())
.setGrantTypes(GrantType.values()[client.getGrantTypes()].getType())
.setResponseTypes(ResponseType.values()[client.getResponseTypes()].getType())
.setCodeExpiresIn(client.getCodeExpiresIn())
.setIdTokenExpiresIn(client.getIdTokenExpiresIn())
.setAccessTokenExpiresIn(client.getAccessTokenExpiresIn())
.setRefreshTokenExpiresIn(client.getRefreshTokenExpiresIn())
.setAutoApprove(client.getAutoApprove())
.setEnablePkce(client.getEnablePkce())
.setCodeChallengeMethod(client.getCodeChallengeMethod());
}
}
package com.liusuyun.flowersay.gateway.services;
import com.fujieid.jap.ids.config.JwtConfig;
import com.fujieid.jap.ids.service.IdsIdentityService;
import org.noear.solon.annotation.Component;
/**
* @author 颖
* @version 1.0.0
* @date 2021-04-16 16:32
* @since 1.0.0
*/
public class IdsIdentityServiceImpl implements IdsIdentityService {
/**
* Get the jwt token encryption key string
*
* @param identity User/organization/enterprise identification
* @return Encryption key string in json format
*/
@Override
public String getJwksJson(String identity) {
return IdsIdentityService.super.getJwksJson(identity);
}
/**
* Get the configuration of jwt token encryption
*
* @param identity User/organization/enterprise identification
* @return Encryption key string in json format
*/
@Override
public JwtConfig getJwtConfig(String identity) {
return IdsIdentityService.super.getJwtConfig(identity);
}
}
package com.liusuyun.flowersay.gateway.services;
import com.fujieid.jap.ids.model.UserInfo;
import com.fujieid.jap.ids.service.IdsUserService;
import com.liusuyun.flowersay.common.entities.models.User;
import org.noear.solon.annotation.Component;
/**
* @author 颖
* @version 1.0.0
* @date 2021-04-14 10:27
* @since 1.0.0
*/
public class IdsUserServiceImpl extends JapService implements IdsUserService {
/**
* Login with account and password
*
* @param username account number
* @param password password
* @return UserInfo
*/
@Override
public UserInfo loginByUsernameAndPassword(String username, String password, String clientId) {
User user = this.findUser(username);
if (user != null) {
if (user.authenticate(password)) {
return this.convert(user);
}
}
return null;
}
/**
* Get user info by userid.
*
* @param userId userId of the business system
* @return UserInfo
*/
@Override
public UserInfo getById(String userId) {
return this.convert(
this.findUser(Long.parseLong(userId))
);
}
/**
* Get user info by username.
*
* @param username username of the business system
* @return UserInfo
*/
@Override
public UserInfo getByName(String username, String clientId) {
return this.convert(
this.findUser(username)
);
}
private UserInfo convert(User user) {
if (user == null) {
return null;
}
return new UserInfo()
.setId(String.valueOf(user.getId()))
.setUsername(user.getUsername())
.setEmail(user.getEmail());
}
}
package com.liusuyun.flowersay.gateway.services;
import com.fujieid.jap.sso.JapMfaService;
import com.liusuyun.flowersay.common.entities.models.User;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author 颖
*/
public class JapMfaServiceImpl extends JapService implements JapMfaService {
/**
* 根据帐号查询 secretKey
*
* @param userName 申请 secretKey 的用户
* @return secretKey
*/
@Override
public String getSecretKey(String userName) {
User user = this.findUser(userName);
return user == null ? null : user.getSecret();
}
/**
* 将 secretKey 关联 userName 后进行保存,可以存入数据库也可以存入其他缓存媒介中
*
* @param userName 用户名
* @param secretKey 申请到的 secretKey
* @param validationCode 当前计算出的 TOTP 验证码
* @param scratchCodes scratch 码
*/
@Override
public void saveUserCredentials(String userName, String secretKey, int validationCode, List<Integer> scratchCodes) {
User user = this.findUser(userName);
if(user == null) {
throw new IllegalArgumentException();
}
user.setSecret(secretKey);
this.userMapper.updateById(user);
}
}
我们重写了 SocialStrategy 实现了登录用户也可以绑定社交账号的能力。
感谢 @738628035 的提醒,对 WeChat 系列产品进行统一处理,使用 unionId 代替默认的 openId,来减少 微信公众号 和 微信 分家的的隐患(雾
package com.liusuyun.flowersay.gateway.services;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.fujieid.jap.core.JapUser;
import com.fujieid.jap.core.JapUserService;
import com.liusuyun.flowersay.common.entities.models.User;
import com.liusuyun.flowersay.common.entities.models.UserBinding;
import com.liusuyun.flowersay.common.mappers.UserBindingMapper;
import me.zhyd.oauth.config.AuthDefaultSource;
import me.zhyd.oauth.model.AuthUser;
import org.apache.ibatis.ext.solon.Db;
import org.mindrot.jbcrypt.BCrypt;
import org.noear.solon.annotation.Component;
import org.noear.solon.core.handle.Context;
import java.util.Locale;
/**
* @author 颖
*/
public class JapUserServiceImpl extends JapService implements JapUserService {
@Db
UserBindingMapper userBindingMapper;
public static String WE_CHAT_SOURCE_PREFIX = "WECHAT";
@Override
public JapUser getById(String userId) {
return this.convert(
this.findUser(Long.parseLong(userId))
);
}
@Override
public JapUser getByName(String username) {
return this.convert(
this.findUser(username)
);
}
@Override
public boolean validPassword(String password, JapUser user) {
boolean success = BCrypt.checkpw(password, user.getPassword());
if (success) {
// 删除敏感数据
user.setPassword(null);
}
return success;
}
/**
* 根据第三方平台标识(platform)和第三方平台的用户 uid 查询数据库
*
* @param platform 第三方平台标识
* @param uid 第三方平台的用户 uid
* @return JapUser
*/
@Override
public JapUser getByPlatformAndUid(String platform, String uid) {
QueryWrapper<UserBinding> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("platform", AuthDefaultSource.valueOf(platform.toUpperCase(Locale.ROOT)).ordinal());
queryWrapper.eq("open_id", uid);
UserBinding userBinding = this.userBindingMapper.selectOne(queryWrapper);
if (userBinding == null) {
return null;
}
return this.convert(
this.findUser(userBinding.getUserId())
);
}
/**
* 创建并获取第三方用户,相当于第三方登录成功后,将授权关系保存到数据库(开发者业务系统中 social user -> sys user 的绑定关系)
*
* @param userInfo JustAuth 中的 AuthUser
* @return JapUser
*/
@Override
public JapUser createAndGetSocialUser(Object userInfo) {
AuthUser authUser = (AuthUser) userInfo;
// 对 WeChat 系列产品的用户进行特殊处理
if(authUser.getSource().toUpperCase(Locale.ROOT).startsWith(JapUserServiceImpl.WE_CHAT_SOURCE_PREFIX)) {
String unionId = authUser.getRawUserInfo().getString("unionId");
if(unionId != null) {
authUser.setUuid(unionId);
}
}
// 查询绑定关系,确定当前用户是否已经登录过业务系统
JapUser user = this.getByPlatformAndUid(authUser.getSource(), authUser.getUuid());
if (user == null) {
// 判断用户是否登录
user = (JapUser) Context.current().session("_jap:session:user");
if (user == null) {
return null;
}
user.setAdditional(authUser);
// 添加用户
this.userBindingMapper.insert(UserBinding.builder()
.userId(Long.valueOf(user.getUserId()))
.platform(AuthDefaultSource.valueOf(authUser.getSource().toUpperCase(Locale.ROOT)).ordinal())
.openId(authUser.getUuid())
.metadata(authUser.getRawUserInfo())
.build()
.buildDate());
}
return user;
}
private JapUser convert(User user) {
if (user == null) {
return null;
}
return new JapUser()
.setUserId(String.valueOf(user.getId()))
.setUsername(user.getUsername())
.setPassword(user.getPassword())
.setAdditional(user);
}
}
其中涉及的的模型以及数据表如下:
Something~
package com.liusuyun.flowersay.gateway.entities;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.liusuyun.flowersay.common.entities.models.Model;
import lombok.*;
/**
* @author 颖
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@TableName(value = "oauth_clients")
@EqualsAndHashCode(callSuper = true)
public class OAuthClient extends Model {
@TableId(type = IdType.AUTO)
private Long id;
private Long userId;
private String name;
private String clientId;
private String clientSecret;
private String siteDomain;
private String redirectUri;
private String logoutUri;
private String logo;
private boolean available;
private String description;
private String scopes;
private int grantTypes;
private int responseTypes;
private Long codeExpiresIn;
private Long idTokenExpiresIn;
private Long accessTokenExpiresIn;
private Long refreshTokenExpiresIn;
private Boolean autoApprove;
private Boolean enablePkce;
private String codeChallengeMethod;
}
/*
Navicat Premium Data Transfer
Source Server : FlowerSay
Source Server Type : MySQL
Source Server Version : 80028
Source Host : localhost:3306
Source Schema : flowersay
Target Server Type : MySQL
Target Server Version : 80028
File Encoding : 65001
Date: 23/02/2022 18:47:41
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for oauth_clients
-- ----------------------------
DROP TABLE IF EXISTS `oauth_clients`;
CREATE TABLE `oauth_clients` (
`id` bigint UNSIGNED NOT NULL AUTO_INCREMENT,
`user_id` bigint UNSIGNED NULL DEFAULT NULL,
`name` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL,
`client_id` varchar(100) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL,
`client_secret` varchar(100) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL,
`site_domain` text CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL,
`redirect_uri` text CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL,
`logout_uri` text CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL,
`available` tinyint(1) NULL DEFAULT NULL,
`logo` text CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL,
`description` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL,
`scopes` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL,
`grant_types` tinyint(1) NOT NULL,
`response_types` tinyint(1) NULL DEFAULT NULL,
`code_expires_in` bigint NULL DEFAULT NULL,
`id_token_expires_in` bigint NULL DEFAULT NULL,
`access_token_expires_in` bigint NULL DEFAULT NULL,
`refresh_token_expires_in` bigint NULL DEFAULT NULL,
`auto_approve` tinyint(1) NULL DEFAULT NULL,
`enable_pkce` tinyint(1) NULL DEFAULT NULL,
`code_challenge_method` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
INDEX `oauth_clients_user_id_index`(`user_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_unicode_ci ROW_FORMAT = DYNAMIC;
SET FOREIGN_KEY_CHECKS = 1;
package com.liusuyun.flowersay.common.entities.models;
import com.alibaba.fastjson.annotation.JSONField;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.liusuyun.flowersay.common.entities.Identity;
import com.liusuyun.flowersay.common.mappers.UserProfileMapper;
import lombok.*;
import org.mindrot.jbcrypt.BCrypt;
import org.noear.solon.validation.annotation.NotNull;
/**
* @author 颖
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@TableName(value = "users")
@EqualsAndHashCode(callSuper = true)
public class User extends Model implements Identity {
@NotNull
@TableId(type = IdType.AUTO)
private Long id;
private String uuid;
private String email;
private String username;
@JSONField(serialize = false)
private String password;
@JSONField(serialize = false)
private String secret;
public UserProfile profile() {
return this.hasOne(UserProfileMapper.class, "id", "id");
}
public User encrypt() {
this.password = BCrypt.hashpw(this.password, BCrypt.gensalt());
return this;
}
public boolean authenticate(String password) {
return BCrypt.checkpw(password, this.password);
}
}
/*
Navicat Premium Data Transfer
Source Server : FlowerSay
Source Server Type : MySQL
Source Server Version : 80028
Source Host : localhost:3306
Source Schema : flowersay
Target Server Type : MySQL
Target Server Version : 80028
File Encoding : 65001
Date: 23/02/2022 18:48:50
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for users
-- ----------------------------
DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
`id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'UID',
`uuid` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT 'UUID',
`email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '邮箱',
`username` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '用户名',
`password` binary(60) NOT NULL COMMENT '密码',
`secret` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT 'Mfa Secret',
`created_at` timestamp NOT NULL COMMENT '创建时间',
`updated_at` timestamp NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `unique`(`email`, `username`) USING BTREE,
INDEX `id`(`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC;
SET FOREIGN_KEY_CHECKS = 1;
package com.liusuyun.flowersay.common.entities.models;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.FastjsonTypeHandler;
import lombok.*;
/**
* @author 颖
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@TableName(value = "user_bindings")
@EqualsAndHashCode(callSuper = true)
public class UserBinding extends Model {
@TableId(type = IdType.AUTO)
private Long id;
private Long userId;
private Integer platform;
private String openId;
@TableField(typeHandler = FastjsonTypeHandler.class)
private JSONObject metadata;
}
/*
Navicat Premium Data Transfer
Source Server : FlowerSay
Source Server Type : MySQL
Source Server Version : 80028
Source Host : localhost:3306
Source Schema : flowersay
Target Server Type : MySQL
Target Server Version : 80028
File Encoding : 65001
Date: 23/02/2022 18:48:50
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for user_bindings
-- ----------------------------
DROP TABLE IF EXISTS `user_bindings`;
CREATE TABLE `user_bindings` (
`id` bigint UNSIGNED NOT NULL AUTO_INCREMENT,
`user_id` bigint UNSIGNED NOT NULL,
`platform` tinyint NOT NULL,
`open_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
`metadata` json NOT NULL,
`created_at` datetime NOT NULL,
`updated_at` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`) USING BTREE,
INDEX `user_bindings_ibfk_1`(`user_id`) USING BTREE,
INDEX `platform`(`platform`) USING BTREE,
INDEX `open_id`(`open_id`) USING BTREE,
CONSTRAINT `user_bindings_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
在上述服务实现后,你还需要通过 Solon 的 Aop 将实现注入进去:
Just Auth Plus 本身具使用 ServiceLoader 加载数据的能力,但是由于 Solon 的类加载机制在某些情况下可能不能实现加载,所以建议使用一定会成功的 Aop 注入方式。
由于 JapMfaService 注入名称为 JapMfaService,所以不能直接将 JapMfaServcieImpl 当做名称注入进去。
package com.liusuyun.flowersay.gateway.configurations;
import com.fujieid.jap.ids.context.IdsContext;
import com.fujieid.jap.ids.solon.IdsCacheImpl;
import com.fujieid.jap.sso.JapMfaService;
import com.liusuyun.flowersay.gateway.services.IdsClientDetailServiceImpl;
import com.liusuyun.flowersay.gateway.services.IdsIdentityServiceImpl;
import com.liusuyun.flowersay.gateway.services.IdsUserServiceImpl;
import com.liusuyun.flowersay.gateway.services.JapMfaServiceImpl;
import org.noear.solon.annotation.Bean;
import org.noear.solon.annotation.Configuration;
import org.noear.solon.annotation.Inject;
import org.noear.solon.core.Aop;
import org.noear.solon.core.BeanWrap;
/**
* @author 颖
*/
@Configuration
public class JapConfig {
@Bean
public void ids(@Inject IdsContext context) {
context.setCache(Aop.getOrNew(IdsCacheImpl.class));
context.setClientDetailService(Aop.getOrNew(IdsClientDetailServiceImpl.class));
context.setIdentityService(Aop.getOrNew(IdsIdentityServiceImpl.class));
context.setUserService(Aop.getOrNew(IdsUserServiceImpl.class));
}
@Bean
public void core() {
JapMfaServiceImpl japMfaService = Aop.getOrNew(JapMfaServiceImpl.class);
Aop.wrapAndPut(JapMfaService.class, japMfaService);
}
}
内置 Controller 概览
你可以在每一个 Just Auth Plus 请求后携带参数 next={} 来标明这次请求不属于前后端分离的请求,相关操作完成后将会跳转到 next 指定的地址而不是直接返回 JSON 数据。Next 地址白名单配置见上方配置文件详解。!!! /auth/social/{platform} !!! 请求必须携带 next 参数用于第三方回调后的跳转。
在每一次请求跳转后,你可以使用
Context.current().session(JAP_LAST_RESPONSE_KEY);
,获取该请求中上一个产生的 JapResponse。
- Just Auth Plus
- POST /auth/login
- GET/POST /auth/social/{platform}
- GET /account/current
- GET /auth/mfa/generate
- POST /auth/mfa/verify
- Just Auth Plus Identity Server
- GET 服务发现:/.well-known/openid-configuration
- GET 解密公钥:/.well-known/jwks.json
- GET 获取授权:/oauth/authorize 跳转页面(登录页面或者回调页面)
- POST 同意授权:/oauth/authorize 同意授权(在确认授权之后)
- GET 自动授权:/oauth/authorize/auto 自动授权(不会显示确认授权页面)
- GET 确认授权:/oauth/confirm 登录完成后的确认授权页面
- GET/POST 获取/刷新Token:/oauth/token
- GET/POST 收回Token:/oauth/revoke_token
- GET/POST 用户详情:/oauth/userinfo
- GET check session:/oauth/check_session
- GET 授权异常:/oauth/error
- GET 登录:/oauth/login 跳转到登录页面
- POST 登录:/oauth/login 执行登录表单
- GET 退出登录:/oauth/logout
撒花~