概述

将 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

撒花~

最后修改:2022 年 03 月 25 日
如果觉得我的文章对你有用,请随意赞赏