Solon v3.0.3

jap-solon-plugin

</> markdown

此插件,由社区成员(浅念)贡献

<dependency>
    <groupId>org.noear</groupId>
    <artifactId>jap-solon-plugin</artifactId>
</dependency>

1、概述

Just Auth Plus 向内为 Solon 提供了 密码登录、社会化登录、Mfa二次验证等其它身份验证的能力。但是请注意 Just Auth Plus 方面目前只完成了 Simple、Socail 以及 Mfa 部分,如果你对 OICD、LADP、HTTP API 的适配有需求,请添加 QQ 群 22200020,大鸽子浅念子会光速适配。

2、配置示例

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}

3、应用示例

我们推荐封装一个公共 Service 用于用户查询,因为会反复使用相同代码。

/**
 * @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);
    }

}

Mfa 服务具体实现

/**
 * @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,来减少 微信公众号 和 微信 分家的的隐患(雾

/**
 * @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);
    }

}

在上述服务实现后,你还需要通过 Solon 的 Aop 将实现注入进去:

Just Auth Plus 本身具使用 ServiceLoader 加载数据的能力,但是由于 Solon 的类加载机制在某些情况下可能不能实现加载,所以建议使用一定会成功的 Aop 注入方式。

由于 JapMfaService 注入名称为 JapMfaService,所以不能直接将 JapMfaServcieImpl 当做名称注入进去。

/**
 * @author 颖
 */
@Configuration
public class JapConfig {
    
    @Bean 
    public void core() {
        JapMfaServiceImpl japMfaService = Solon.context().getBeanOrNew(JapMfaServiceImpl.class);
        Solon.context().wrapAndPut(JapMfaService.class, japMfaService);
    }

}

内置 Controller 概览

你可以在每一个 Just Auth Plus 请求后携带参数 next={} 来标明这次请求不属于前后端分离的请求,相关操作完成后将会跳转到 next 指定的地址而不是直接返回 JSON 数据。Next 地址白名单配置见上方配置文件详解。!!! /auth/social/{platform} !!! 请求必须携带 next 参数用于第三方回调后的跳转。

在每一次请求跳转后,你可以使用 Context.current().session(JAP_LAST_RESPONSE_KEY);,获取该请求中上一个产生的 JapResponse。

POST /auth/login

GET/POST /auth/social/{platform}

GET /account/current

GET /auth/mfa/generate

POST /auth/mfa/verify