开源OAuth2框架 实现SSO单点登录

一、 概述

本文使用Oauth2开源架构(tkey)、实现单点登录系统。

1. Tkey:

  • OAuth 2.0 标准为接口设计原则的单点登录系统(SSO);
  • 纯粹的 HTTP,任意设备、任意场景;
  • 跨域无状态,随意横向扩展,服务高可用。

2. 选择Tkey

tkey为开源框架,使用方便,易于扩展,完成度高,文档详细。

本文在原有架构基础上,增加了服务端持久层、增加client管理、增加登录页账套查询、优化登录页面、实现客户端跳转。

更多功能请参考原架构

3. Tkey下载地址:

本文代码地址,详见文末。

二、实现单点登录服务端

1)使用架构

  1. springboot 2.1
  2. mybatisPlus  (mysql)
  3. tkey (oauth2)
  4. swagger
  5. redis
  6. thymeleaf(登录页)

2)代码参考如下

以下着重介绍修改部分:

  • 增加持久层 mybatis
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.0.5</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
@EnableTransactionManagement
@Configuration
public class MybatisPlusConfig {

    /**
     * 分页插件
     */
    @Bean
    public PaginationInterceptor paginationInterceptor() {
        return new PaginationInterceptor();
    }
}
  • ClientController.java
@Slf4j
@RestController
@RequestMapping("/client")
public class ClientController {
	@Autowired
	private StringRedisService<String, String> clientRedisService;

	@Autowired
	private OauthClientService oauthClientService;

	@PostMapping("/save")
	public ResponseEntity<?> save(@RequestBody @Valid OauthClientCreateRequestParam param) {
		OauthClientToRedisBO oauthClientToRedisBO = new OauthClientToRedisBO();
		BeanUtils.copyProperties(param, oauthClientToRedisBO);
		clientRedisService.set(GlobalVariable.REDIS_CLIENT_ID_KEY_PREFIX + oauthClientToRedisBO.getClientId(), JsonUtil.toJson(oauthClientToRedisBO));
		return R.success(oauthClientToRedisBO);
	}

	@GetMapping("/query")
	private ResponseEntity<?> query(@RequestParam String clientId) {
		return R.success(oauthClientService.findByClientId(clientId));
	}

	@GetMapping("/delete")
	private ResponseEntity<?> delete(@RequestParam String clientId) {
		String clientIdRedisKey = GlobalVariable.REDIS_CLIENT_ID_KEY_PREFIX + clientId;
		clientRedisService.delete(clientIdRedisKey);
		return R.success();
	}
}
  • 登录初始化

  • 登录校验

  • 登录页面
<div class="login-wrapper">

    <div class="content-left">

    </div>
    <div class="content-right">
        <div class="right-header">
            <h1 class="right-header-h2" th:text="${oauthClient != null and oauthClient.clientName != null ? oauthClient.clientName : '登录'}"></h1>
        </div>
        <div class="right-form-wrapper">
            <form onclick="this.disabled=false" onsubmit="return handleSubmit()" method="POST" action="/oauth/authorize" th:action="${#request.getQueryString() != null ? #request.getRequestURL() + '?' + #request.getQueryString() : #request.getRequestURL()}">
                <div class="username-wrapper">
                    <input onfocus="usernameInputFocus()" class="username username-input-default" type="text" name="username" id="username" value="admin" th:value="admin" placeholder="手机 / 邮箱">
                    <p class="username-error opacity0" th:text="${errorMsg}"></p>
                </div>

                <div class="password-wrapper">
                    <input onfocus="passwordInputFocus()" class="password password-input-default" type="password" name="password" id="password" value="123456" th:value="123456" placeholder="请输入密码">
                    <p class="password-error opacity0">密码为必填项</p>
                </div>
                <div class="form-group">
                    <select class="username username-input-default" name="sob" id="sob">
                        <option th:each="ss:${sobs}" th:value="${ss.id}" th:text="${ss.sobName}"></option>
                    </select>
                </div>
                <div class="checkMe">
                    <span>
                        <input type="checkbox" value="false" name="bool_is_remember_me" id="bool_is_remember_me">
                        <label for="bool_is_remember_me" class="ml5">记住我</label>
                    </span>
                    <a class="text-login">忘记密码?</a>
                </div>

                <div class="submit-btn-wrapper">
                    <img class="btn-loading" th:src="@{/images/loading.svg}" alt="">
                    <button type="submit" onclick="handleSubmit()" class="submit-btn">登录</button>
                </div>
                <div class="mt15 tac">
                    <span class="right-header-span">没有账号? <a>点此注册</a></span>
                </div>
            </form>
        </div>
    </div>
</div>

 


三、实现客户端

  • pom.xml
        <!-- sso -->
        <dependency>
            <groupId>com.cdk8s.tkey</groupId>
            <artifactId>tkey-sso-client-starter-rest</artifactId>
            <version>1.0.0</version>
        </dependency>
  • 自定义拦截器
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
    @Bean
    public LoginInterceptor loginInterceptor() {
        return new LoginInterceptor();
    }


    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor())
                .addPathPatterns("/**")
                .excludePathPatterns("/error", "/logoutSuccess", "login/**","/logout/**","/codeCallback/**","/css/**", "/js/**", "/fonts/**")
                .excludePathPatterns("/swagger-resources/**", "/webjars/**", "/v2/**", "/swagger-ui.html/**");
    }
}

@Slf4j
public class LoginInterceptor extends HandlerInterceptorAdapter {

    @Value("${front_url}")
    private String front_url;

    @Autowired
    private TkeyProperties tkeyProperties;

    //=====================================业务处理 start=====================================
    private boolean resp(final HttpServletRequest request, final HttpServletResponse response) throws IOException {
        if (tkeyProperties.getEnableCodeCallbackToFront()) {
            responseJson(response);
        } else {
            response.sendRedirect(getRedirectUrl(request));
        }
        return false;
    }

    @SneakyThrows
    @Override
    public boolean preHandle(final HttpServletRequest request, final HttpServletResponse response, Object handler) {

        String accessToken = CookieUtil.getValue(request, AuthVariable.SSO_SESSIONID);
        if (StringUtils.isBlank(accessToken)) {
            log.error("【SSO】Local token is null, to login...");
            return resp(request, response);
        }
        // 本机不校验
        String local_url = InetAddress.getLocalHost().getHostAddress();
        if (request.getRemoteAddr().contains(local_url)) {
            log.info("【SSO】Local to pass...");
            return true;
        }
        String token = request.getHeader(AuthVariable.HEADER_TOKEN_KEY);
        if (StringUtils.isBlank(token) || !token.equals(accessToken)) {
            response.sendRedirect(front_url + accessToken);
            return false;
        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {
    }


    private String getRedirectUrl(final HttpServletRequest request) {
        return tkeyProperties.getFinalRedirectUri(request);
    }

    @SneakyThrows
    private void responseJson(final HttpServletResponse response) {
        response.setStatus(HttpStatus.UNAUTHORIZED.value());
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        Map<String, Object> responseMap = new HashMap<>(4);
        responseMap.put("isSuccess", false);
        responseMap.put("msg", "您还未登录,请先登录");
        responseMap.put("timestamp", Instant.now().toEpochMilli());
        responseMap.put("code", "0");
        ObjectMapper mapper = new ObjectMapper();
        String json = mapper.writeValueAsString(responseMap);
        PrintWriter out = response.getWriter();
        out.print(json);
    }

}
  • 鉴权回调controller

   /**
     * 接收 code,然后换取 token
     */
    @SneakyThrows
    @RequestMapping(value = "/codeCallback", method = RequestMethod.GET)
    public void codeCallback(final HttpServletRequest request, final HttpServletResponse response, @RequestParam(value = "redirect_uri", required = true) String redirectUri) {
        String code = request.getParameter("code");

        if (StringUtils.isBlank(code)) {
            return;
        }

        getAccessToken(request, response, code);
        // 重定向到原请求地址
        redirectUri = CodecUtil.decodeURL(redirectUri);
		response.sendRedirect(redirectUri);
    }

    @RequestMapping(value = "/logout", method = RequestMethod.GET)
    public String logout(final HttpServletRequest request, final HttpServletResponse response) {
        String finalLogoutUri = tkeyProperties.getFinalLogoutUri();
        CookieUtil.remove(request, response, AuthVariable.SSO_SESSIONID);
        return "redirect:" + finalLogoutUri;
    }


    private TkeyToken getAccessToken(HttpServletRequest request, HttpServletResponse response, String code) {
        OAuth2AccessToken oauthToken = tkeyService.getAccessToken(code);
        String accessToken = oauthToken.getAccessToken();

        TkeyToken tkeyToken = new TkeyToken();
        tkeyToken.setAccessToken(accessToken);
        tkeyToken.setRefreshToken(oauthToken.getRefreshToken());
        OauthUserProfile user = tkeyService.getUserProfile(oauthToken);
        tkeyToken.setAttributes(user);

        CookieUtil.set(response, AuthVariable.SSO_SESSIONID, accessToken, false);
        return tkeyToken;
    }
  • 首页controller(支持前后端分离)

    @RequestMapping("/")
    public String index(HttpServletRequest request, Model model) {
        String accessToken = CookieUtil.getValue(request, AuthVariable.SSO_SESSIONID);
        if (front_flag) {
            return "redirect:" + front_url + accessToken;
        } else {
            model.addAttribute("token", accessToken);
            return "index";
        }
    }

四、启动程序

1. 启动redis;

2. 启动mysql:创建数据库,运行脚本。

注:脚本参考文末代码

3. 启动程序. ServerApp、2.ClientApp

客户端port:8081

服务端port:8086

4. 插入client:

调用接口如图:

报文如下:

{
        "id": 1001,
        "client_name": "client_id_sso_client",
        "client_id": "client_id_sso_client",
        "client_secret": "client_secret_sso_client",
        "client_url": "^(http|https)://.*",
        "client_desc": "Client系统"
}

5. 调用客户端

在浏览器地址栏输入:http://127.0.0.1:8081/client

统一跳转到鉴权服务器,如图

注:账套为本文自定义信息,可酌情删除。

用户名:admin   、密码:123456

登录成功:

以上,登录成功。

注:如果使用前后端分离系统,请在配置中增加前端首页地址

登录成功后,将token返回前端即可。

 



本文源码地址:

https://gitee.com/zetor2020/ym-paas-sso-tkey.git 

下载代码的朋友点下star,多谢支持

点赞本文,再次感谢

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值