jap/jap-ids
2021-09-15 11:13:29 +08:00
..
src 📝 change: Optimize code 2021-09-15 11:13:29 +08:00
pom.xml feat: add HTTP servlet adapter to decouple jakarta servlets 2021-09-14 23:54:53 +08:00
README.md 🔥 Remove TokenEndpoint.checkToken 2021-04-19 18:53:23 +08:00

如何通过 jap-ids 实现 OAuth 2.0 + OIDC 协议?

完整示例代码参考:https://gitee.com/fujieid/jap-ids-demo

第一步:实现 ids 接口

  1. 实现 IdsClientDetailService.java
package com.fujieid.ids.demo.service;

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.provider.IdsScopeProvider;
import com.fujieid.jap.ids.service.IdsClientDetailService;
import com.fujieid.jap.ids.util.OauthUtil;
import org.springframework.stereotype.Service;

import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;

/**
 * @author yadong.zhang (yadong.zhang0415(a)gmail.com)
 * @version 1.0.0
 * @date 2021-04-14 10:27
 * @since 1.0.0
 */
@Service
public class IdsClientDetailServiceImpl implements IdsClientDetailService {
    public List<ClientDetail> clientDetails = new LinkedList<>();

    public IdsClientDetailServiceImpl() {
        clientDetails.add(new ClientDetail()
                .setId("1")
                .setAppName("适应所有场景")
                .setClientId(OauthUtil.generateClientId())
                .setClientSecret(OauthUtil.generateClientSecret())
                .setRedirectUri("http://localhost:8080")
                .setScopes(String.join(" ", IdsScopeProvider.getScopeCodes()))
                .setGrantTypes(String.join(" ", GrantType.grantTypes()))
                .setResponseTypes(String.join(" ", Arrays.asList(ResponseType.CODE.getType(), ResponseType.TOKEN.getType(), ResponseType.ID_TOKEN.getType(), ResponseType.NONE.getType())))
                .setAvailable(true)
        );
    }

    /**
     * 通过 client_id 查询客户端信息
     *
     * @param clientId 客户端应用id
     * @return AppOauthClientDetails
     */
    @Override
    public ClientDetail getByClientId(String clientId) {
        return clientDetails.stream().filter(client -> client.getClientId().equals(clientId)).findFirst().orElse(null);
    }

    /**
     * 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) {
        return IdsClientDetailService.super.removeById(id);
    }

    /**
     * Delete client by client id
     *
     * @param clientId Client application id
     * @return ClientDetail
     */
    @Override
    public boolean removeByClientId(String clientId) {
        return IdsClientDetailService.super.removeByClientId(clientId);
    }

    /**
     * 获取所有 client detail
     *
     * @return List
     */
    @Override
    public List<ClientDetail> getAllClientDetail() {
        return clientDetails;
    }
}
  1. 实现 IdsIdentityService.java
package com.fujieid.ids.demo.service;

import com.fujieid.jap.ids.config.JwtConfig;
import com.fujieid.jap.ids.service.IdsIdentityService;
import org.springframework.stereotype.Service;

/**
 * @author yadong.zhang (yadong.zhang0415(a)gmail.com)
 * @version 1.0.0
 * @date 2021-04-16 16:32
 * @since 1.0.0
 */
@Service
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);
    }
}
  1. 实现 IdsUserService.java
package com.fujieid.ids.demo.service;

import com.fujieid.jap.ids.model.UserInfo;
import com.fujieid.jap.ids.service.IdsUserService;
import org.springframework.stereotype.Service;

import java.util.LinkedList;
import java.util.List;

/**
 * @author yadong.zhang (yadong.zhang0415(a)gmail.com)
 * @version 1.0.0
 * @date 2021-04-14 10:27
 * @since 1.0.0
 */
@Service
public class IdsUserServiceImpl implements IdsUserService {
    public List<UserInfo> userInfoList = new LinkedList<>();

    public IdsUserServiceImpl() {
        for (int i = 0; i < 10; i++) {
            UserInfo userInfo = new UserInfo();
            userInfoList.add(userInfo
                    .setId(i + "")
                    .setUsername("test"));
        }
    }

    @Override
    public UserInfo loginByUsernameAndPassword(String username, String password) {
        return userInfoList.stream().filter(userInfo -> userInfo.getUsername().equals(username)).findFirst().orElse(null);
    }

    /**
     * Get user info by userid.
     *
     * @param userId userId of the business system
     * @return JapUser
     */
    @Override
    public UserInfo getById(String userId) {
        return userInfoList.stream().filter(userInfo -> userInfo.getId().equals(userId)).findFirst().orElse(null);
    }

    /**
     * Get user info by username.
     * <p>
     * It is suitable for the {@code jap-simple} module
     *
     * @param username username of the business system
     * @return JapUser
     */
    @Override
    public UserInfo getByName(String username) {
        return userInfoList.stream().filter(userInfo -> userInfo.getUsername().equals(username)).findFirst().orElse(null);
    }
}

注:jap-ids 默认提供了基于内存的缓存方案,如果不能满足你的业务场景,可以实现自定义的缓存:

package com.fujieid.ids.demo.service;

import com.fujieid.jap.core.cache.JapCache;

import java.io.Serializable;

/**
 * @author yadong.zhang (yadong.zhang0415(a)gmail.com)
 * @version 1.0.0
 * @date 2021-04-17 20:06
 * @since 1.0.0
 */
public class IdsCacheImpl implements JapCache {
    /**
     * Set cache
     *
     * @param key   Cache key
     * @param value Cache value after serialization
     */
    @Override
    public void set(String key, Serializable value) {
        
    }

    /**
     * Set the cache and specify the expiration time of the cache
     *
     * @param key     Cache key
     * @param value   Cache value after serialization
     * @param timeout The expiration time of the cache, in milliseconds
     */
    @Override
    public void set(String key, Serializable value, long timeout) {

    }

    /**
     * Get cache value
     *
     * @param key Cache key
     * @return Cache value
     */
    @Override
    public Serializable get(String key) {
        return null;
    }

    /**
     * Determine whether a key exists in the cache
     *
     * @param key Cache key
     * @return boolean
     */
    @Override
    public boolean containsKey(String key) {
        return false;
    }

    /**
     * Delete the key from the cache
     *
     * @param key Cache key
     */
    @Override
    public void removeKey(String key) {

    }
}

然后通过 IdsContextsetCache 方法设置自定义的 cache 实现。

第二步:注册 ids 上下文

通过 JapIds.registerContext 方法注册 ids 上下文,配置全局参数。

// 注册 JAP IDS 上下文
JapIds.registerContext(new IdsContext()
    .setUserService(idsUserService)
    .setClientDetailService(idsClientDetailService)
    .setIdentityService(idsIdentityService)
    .setIdsConfig(new IdsConfig()
        .setIssuer("http://localhost:" + port)
        .setJwtConfig(new JwtConfig()
            .setJwksKeyId("jap-jwk-keyid")
            .setJwksJson("{\n" +
                "    \"keys\": [\n" +
                "        {\n" +
                "            \"p\": \"v5G0QPkr9zi1znff2g7p5K1ac1F2KNjXmk31Etl0UrRBwHiTxM_MkkldGlxnXWoFL4_cPZZMt_W14Td5qApknLFOh9iRWRPwqlFgC-eQzUjPeYvxjRbtV5QUHtbzrDCLjLiSNyhsLXHyi_yOawD2BS4U6sBWMSJlL2lShU7EAaU\",\n" +
                "            \"kty\": \"RSA\",\n" +
                "            \"q\": \"s2X9UeuEWky_io9hFAoHZjBxMBheNAGrHXtWat6zlg2tf_SIKpZ7Xs8C_-kr9Pvj-D428QsOjFZE-EtNBSXoMrvlMk7fGDl9x1dHvLS9GSitkXH2-Wthg8j0j0nfAmyEt94jP-XEkYic1Ok7EfBOPuvL21HO7YuB-cOff9ZGvBk\",\n" +
                "            \"d\": \"Rj-QBeBdx85VIHkwVY1T94ZeeC_Z6Zw-cz5lk5Msw0U9QhSTWo28-d2lYjK7dhQn-E19JhTbCVE11UuUqENKZmO__yRgO1UJaj2x6vWMtgJptah7m8lI-QW0w6TnVxAHWfRPpKSEfbN4SpeufYf5PYhmmzT0A954Z2o0kqS4iHd0gwNAovOXaxriGXO1CcOQjBFEcm0BdboQZ7CKCoJ1D6S0xZpVFSJg-1AtagY5dzStyekzETO2tQSmVw4ogIoJsIbu3aYwbukmCoULQfJ36D0mPzrTG5oocEbbuCps_vH72VjZORHHAl4hwritFT_jD2bdQHSNMGukga8C0L1WQQ\",\n" +
                "            \"e\": \"AQAB\",\n" +
                "            \"use\": \"sig\",\n" +
                "            \"kid\": \"jap-jwk-keyid\",\n" +
                "            \"qi\": \"Asr5dZMDvwgquE6uFnDaBC76PY5JUzxQ5wY5oc4nhIm8UxQWwYZTWq-HOWkMB5c99fG1QxLWQKGtsguXfOXoNgnI--yHzLZcXf1XAd0siguaF1cgQIqwRUf4byofE6uJ-2ZON_ezn6Uvly8fDIlgwmKAiiwWvHI4iLqvqOReBgs\",\n" +
                "            \"dp\": \"oIUzuFnR6FcBqJ8z2KE0haRorUZuLy38A1UdbQz_dqmKiv--OmUw8sc8l3EkP9ctvzvZfVWqtV7TZ4M3koIa6l18A0KKEE0wFVcYlwETiaBgEWYdIm86s27mKS1Og1MuK90gz800UCQx6_DVWX41qAOEDWzbDFLY3JBxUDi-7u0\",\n" +
                "            \"alg\": \"RS256\",\n" +
                "            \"dq\": \"MpNSM0IecgapCTsatzeMlnaZsmFsTWUbBJi86CwYnPkGLMiXisoZxcS-p77osYxB3L5NZu8jDtVTZFx2PjlNmN_34ZLyujWbDBPDGaQqm2koZZSnd_GZ8Dk7GRpOULSfRebOMTlpjU3iSPPnv0rsBDkdo5sQp09pOSy5TqTuFCE\",\n" +
                "            \"n\": \"hj8zFdhYFi-47PO4B4HTRuOLPR_rpZJi66g4JoY4gyhb5v3Q57etSU9BnW9QQNoUMDvhCFSwkz0hgY5HqVj0zOG5s9x2a594UDIinKsm434b-pT6bueYdvM_mIUEKka5pqhy90wTTka42GvM-rBATHPTarq0kPTR1iBtYao8zX-RWmCbdumEWOkMFUGbBkUcOSJWzoLzN161WdYr2kJU5PFraUP3hG9fPpMEtvqd6IwEL-MOVx3nqc7zk3D91E6eU7EaOy8nz8echQLl6Ps34BSwEpgOhaHDD6IJzetW-KorYeC0r0okXhrl0sUVE2c71vKPVVtueJSIH6OwA3dVHQ\"\n" +
                "        }\n" +
                "    ]\n" +
                "}")
            )
        )
    );

这儿有几点要注意:

  1. JwksKeyIdJwksJson(证书)中的 kid 属性要一致;
  2. JwksKeyId 默认为 jap-jwk-keyid
  3. 如果 JwksJson(证书) 中的 kid 不等于 jap-jwk-keyid,则必须要重新 setJwksKeyId
  4. JWK 证书可以从 https://mkjwk.org/ 平台生成;
  5. JapIds.registerContext 方法,必须要在 http 接口请求发起前执行,可以监听项目启动事件, 项目启动成功后立即执行该方法。

第三步:开发相关 http 接口

大概需要以下几个接口:

  • 服务发现http://{host}:{port}/.well-known/openid-configuration
  • 解密公钥http://{host}:{port}/.well-known/jwks.json
  • 获取授权http://{host}:{port}/oauth/authorize
  • 确认授权http://{host}:{port}/oauth/confirm
  • 获取/刷新Tokenhttp://{host}:{port}/oauth/token
  • 收回Tokenhttp://{host}:{port}/oauth/revoke_token
  • 用户详情http://{host}:{port}/oauth/userinfo
  • check sessionhttp://{host}:{port}/oauth/check_session
  • 授权异常http://{host}:{port}/oauth/error
  • 登录http://{host}:{port}/oauth/login
  • 退出登录http://{host}:{port}/oauth/logout

注意,如果你在开发 http 接口时, 没有按照以上示例的路径格式命名,那么,在你开发完成后,需要在第二步:注册 ids 上下文时,重新配置IdsConfig节点下相关属性,如下:

属性名 对应 http 接口 默认值 备注
loginUrl 登录 /oauth/login
errorUrl 授权异常 /oauth/error
confirmUrl 确认授权 /oauth/confirm
authorizeUrl 获取授权 /oauth/authorize
tokenUrl 获取/刷新Token /oauth/token
userinfoUrl 用户详情 /oauth/userinfo
registrationUrl 注册 /oauth/registration (未提供)
endSessionUrl 退出登录 /oauth/logout
checkSessionUrl 校验登录状态 /oauth/check_session
jwksUrl 解密公钥 /.well-known/jwks.json
discoveryUrl 服务发现 /.well-known/openid-configuration

第四步:配置过滤器

jap-ids 默认提供了两类过滤器:

  • Access Token 验权过滤器
  • 用户登录状态过滤器

以本项目为例,配置以下两个过滤器:

Access Token 验权过滤器

@Bean
public FilterRegistrationBean<IdsAccessTokenFilter> registeraccessTokenFilter() {
    FilterRegistrationBean<IdsAccessTokenFilter> registration = new FilterRegistrationBean<>();
    registration.setFilter(new IdsAccessTokenFilter());
    registration.addUrlPatterns("/*");
    registration.addInitParameter("ignoreUrl",
            "/," +
            "/oauth/login," +
            "/oauth/error," +
            "/oauth/confirm," +
            "/oauth/authorize," +
            "/oauth/token," +
            "/oauth/check_session," +
            "/oauth/registration," +
            "/.well-known/jwks.json," +
            "/.well-known/openid-configuration"
    );
    registration.setName("IdsAccessTokenFilter");
    registration.setOrder(1);
    return registration;
}

用户登录状态过滤器

@Bean
public FilterRegistrationBean<IdsUserStatusFilter> registerUserStatusFilter() {
    FilterRegistrationBean<IdsUserStatusFilter> registration = new FilterRegistrationBean<>();
    registration.setFilter(new IdsUserStatusFilter());
    registration.addUrlPatterns("/*");
    registration.addInitParameter("ignoreUrl",
            "/," +
            "/oauth/login," +
            "/oauth/error," +
            "/oauth/confirm," +
            "/oauth/authorize," +
            "/oauth/token," +
            "/oauth/check_session," +
            "/oauth/registration," +
            "/.well-known/jwks.json," +
            "/.well-known/openid-configuration"
    );
    registration.setName("IdsUserStatusFilter");
    registration.setOrder(1);
    return registration;
}

总结

基于以上步骤, 就可快速搭建起来一套本地化的 OAuth2.0 服务。更多功能,请参考 帮助文档