第一步:在keycloak平台上,新建一个client app
联系Keycloak管理员,提供 应用的 root url 和 app name即可。
建好client之后,可以得到一个 client secret(密匙)。
第二步:在client project中 加入 keycloak配置
配置形如:
# 空间名,默认所有app和用户都在一个keycloak空间 keycloak.realm=ops # keycloak服务器的auth地址 keycloak.auth-server-url=http://localhost:8180/auth # client app name keycloak.resource=fm-cache-cloud # client secret keycloak.credentials.secret=d4589683-0ce7-4982-bcd3-c48a12572f79 # 登录url和所需要的role keycloak.securityConstraints[0].authRoles[0] = user keycloak.securityConstraints[0].securityCollections[0].patterns[0] = /manage/ssologin/*
上面的配置,除了 最后一项,其他都是基本配置,直接填好就行了。
最后一项需要先在controller中定义这样一个用于登录的url,下面会讲。
在讲第三步之前,说一下Keycloak客户端接入原理:
(keycloak支持很多种方式接入,我只讲其中一部分)
基本原理:
keycloak是JBOSS开源的,JBOSS是做服务器的,所以,对于服务器,它比谁都玩得熟,Keycloak的强大之处也在于,它对于客户端应用的管控,直接可以到 服务器层面(相当于给服务器装一个插件,然后进入这个服务器的请求,都会被拦截和认证)。
本文以我们常用的Spring Boot 内嵌的 Tomcat 服务器 为例,在项目中引入 keycloak包并配置好之后,实际上开启了一个 tomcat的 filter,这个filter会拦截指定的url,如果没登录,就跳转到统一登录页面进行登录。
(如果不是用的Spring boot内嵌的tomcat服务器,比如用的是独立的tomcat服务器,原理也是一样的,只是配置方法不一样)
keycloak提供了很多种插件(adapter),例如仅Java的adapter就有如下:
2.1.1. Java Adapter Config
2.1.2. JBoss EAP/WildFly Adapter
2.1.4. JBoss Fuse 6、7 Adapter
2.1.6. Spring Boot Adapter
2.1.7. Tomcat 6, 7 and 8 Adapters
2.1.8. Jetty 9.x Adapters
2.1.9. Jetty 8.1.x Adapter
2.1.10. Spring Security Adapter
2.1.11. Java Servlet Filter Adapter
下面以 Spring Boot Adapter为例,说明如何装插件。其他服务器,或者其他语言的客户端是类似的,很简单。
安装方法:
例如 spring boot 1.x,在pom.xml中引入下面依赖即可:
<!-- Keycloak --> <dependency> <groupId>org.keycloak</groupId> <artifactId>keycloak-legacy-spring-boot-starter</artifactId> <version>5.0.0</version> </dependency>
如果是spring boot 2.x版本,上面的 starter 换成 keycloak-spring-boot-starter。
由于keycloak是基于 filter拦截器的,所以如果 项目本身 已经用了filter来作为登录控制的话,则需要进行改造,Java项目常见情况如下:
1、基于shiro框架进行登录控制;
2、基于spring security进行登录控制;
3、基于自定义简单的servlet filter进行登录控制;
下面,针对 2、3 项目情况,说明如何进行集成配置(注意,不同的项目,情况可能不完全一样,只要掌握思路即可)。
第三步(针对“3、基于简单servlet filter登录的项目”)
改造之前:
原项目,采用了filter来拦截请求,如果没登录,则跳转到登录页面(比如 /mange/login)。
使用项目自带的登录页面,进行登录。
改造之后:
沿用原来的filter,但是如果没登录,则跳转到 用于统一登录的指定controller(比如 /mange/ssologin);
把这个统一登录的controller的url,配置成 keycloak拦截的登录地址,使用keycloak来进行登录;
这个controller,逻辑很简单,一个例子如下,流程见注释:
@GetMapping("/ssologin") public View ssologin(HttpServletRequest request) { // 1、从request获取用户名,再查看本系统中有无此用户 // 2、有这个用户,则执行登录成功逻辑,代表登录成功 // 3、没有这个用户,则执行登录失败逻辑,比如跳转到登录页面 }
一个真实例子:
@RequestMapping(value = "/ssologin", method = RequestMethod.GET) public ModelAndView ssologin(HttpServletRequest request, HttpServletResponse response) { // 从request获取用户名,再查看本系统中有无此用户 String userName = Identity(request).getName(); AppUser user = userService.getByName(userName); if (user == null) { return new ModelAndView("redirect:/manage/login"); } else { // 有这个用户,则添加到session或者cookie中,代表登录成功 userLoginService.addLoginStatus(request, response, user.getId().toString()); } // 返回用户主页 return new ModelAndView("redirect:/admin/app/list.do"); }
登录原理 说明:
由于在keycloak配置中加入了url权限控制,如下
# 登录url和所需要的role keycloak...authRoles[0] = user keycloak...patterns[0] = /manage/ssologin/*
那么,访问这个 url,在没登录的情况下,就会跳转到 统一登录页面,用户输入用户密码成功登录之后,就会进入到上面定义的 controller中,再执行应用本地的登录逻辑即可。
退出登录,很简单,只需要执行 HttpServletRequest.logout() 即可
例如:
@GetMapping(value = "/logout") public void logout(HttpServletRequest request) throws ServletException { // 先移除本地的session或者cookie userLoginService.removeLoginStatus(request, response); // 然后执行 request.logout() 即可 request.logout(); }
第三步(针对“2、基于spring security进行登录的项目”)
改造之前:
请求被spring security的UsernamePasswordAuthenticationFilter拦截,判断是否登录,如果未登录,则跳转到项目自己的登录页面。
使用项目自带的登录页面,进行登录。
准备工作:写一个KeycloakAuthenticationFilter,重载spring security的UsernamePasswordAuthenticationFilter,它的逻辑是,先判断有没有用户进行统一登录,如果用户已经统一登录了,但是本地没登录,则进行本地登录。
改造之后:
请求被spring security的KeycloakAuthenticationFilter拦截,判断是否登录,如果未登录,则跳转到 用于统一登录的指定controller(比如 /keycloak/ssologin);
把这个统一登录的controller的url,配置成 keycloak拦截的登录地址,使用keycloak来进行登录;
keycloak登录的controller执行成功之后,再跳转到spring security的登录处理url进行登录。
改造之前,spring security配置如下:
@Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable(); http.headers().frameOptions().disable(); http.authorizeRequests().antMatchers("/openapi/**", "/keycloak/**", "/img/**") .permitAll().antMatchers("/**").hasAnyRole(USER_ROLE); http.formLogin().loginPage("/signin").permitAll().loginProcessingUrl("/sslogin") .failureUrl("/signin?#/error").and().httpBasic(); http.logout().invalidateHttpSession(true).clearAuthentication(true) .logoutSuccessUrl("/signin?#/logout"); http.exceptionHandling().authenticationEntryPoint( new LoginUrlAuthenticationEntryPoint("/signin?#/logout")); }
改造之后,spring security配置如下:
@Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable(); http.headers().frameOptions().disable(); http.authorizeRequests().antMatchers("/openapi/**", "/keycloak/**", "/img/**") .permitAll().antMatchers("/**").hasAnyRole(USER_ROLE); http.formLogin().loginPage("/signin").permitAll().loginProcessingUrl("/sslogin") .failureUrl("/signin?#/error").and().httpBasic().and() // add by zollty .addFilterBefore(keycloakAuthenticationFilter(), BasicAuthenticationFilter.class); http.logout().invalidateHttpSession(true).clearAuthentication(true) .logoutSuccessUrl("/signin?#/logout") // add by zollty .addLogoutHandler(new KeycloakSpringLogoutHandler()); http.exceptionHandling().authenticationEntryPoint( // to keycloak ssologin controller new LoginUrlAuthenticationEntryPoint("/keycloak/ssologin")); }
即,加了一个自定义的 keycloakAuthenticationFilter 和 KeycloakSpringLogoutHandler,同时 将LoginUrlAuthenticationEntryPoint登录地址,修改成 用于统一登录的指定controller的URL。这个controller代码如下:
@RequestMapping(value = "keycloak/ssologin", method = RequestMethod.GET) public ModelAndView ssologin() { return new ModelAndView("redirect:/sslogin"); }
进入到这个方法,代表已经sso登录成功,然后直接跳转到 spring security的loginProcessingUrl进行本地登录即可。
KeycloakSpringLogoutHandler的代码如下:
public class KeycloakSpringLogoutHandler implements LogoutHandler { @Override public void logout(HttpServletRequest request, HttpServletResponse response , Authentication authentication) { // 退出keycloak sso try { request.logout(); } catch (ServletException e) { e.printStackTrace(); } } }
其作用是退出统一登录。
KeycloakAuthenticationFilter的代码如下:
public class KeycloakAuthenticationFilter extends UsernamePasswordAuthenticationFilter { static final String DEFAULT_PASSWD_SIGN = "`kc`"; @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { String username = obtainUsername(request); String password = null; Identity identity = new Identity(request); if (username == null && identity.getSecurityContext() != null) { username = identity.getName(); password = DEFAULT_PASSWD_SIGN; } else { password = obtainPassword(request); if (password == null) { password = ""; } else if(DEFAULT_PASSWD_SIGN.equals(password)) { throw new AuthenticationServiceException("Auth error"); } } username = username.trim(); UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password); setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); } }
这个代码来源于spring security的UsernamePasswordAuthenticationFilter,只是加了7行代码,用于先判断是否有用户已经统一登录过,如果已经统一登录了,则将其密码设为一个特殊字符,这是一个取巧的做法,因为用户已经统一登录了,所以后面只要看到是这个特殊字符,就不再验证密码,直接登录。
其他编程语言应用的接入
方法一:自己根据OpenID Connect和OAuth2.0的原理,找到相应的client,引入自己的项目使用。
官方指导文档:https://www.keycloak.org/docs/latest/securing_apps/index.html#other-openid-connect-libraries
方法二:直接在网上或GitHub上搜索现成的第三方Client、Adapter或Demo,参照配置。例如:
1)Keycloak Golang相关的第三方adapter:
https://github.com/mitch-strong/KeycloakGo
https://github.com/cwocwo/keycloak-adapter-go
更多参见:https://github.com/search?l=Go&q=Keycloak&type=Repositories
2)Python相关的Client、Adapter或Demo:(包括Django、Flask相关的例子都有)
https://github.com/search?l=Python&q=Keycloak&type=Repositories
全文总结
在有keycloak adapter的加持下,keycloak的接入相当简单,它是可以做到不改一行代码的 —— 之所以上面提到一些小的改动,完全是 为了 替换或者 适配 原来项目已有 的登录filter配置,把原来的账号+密码的登录形式,拦截并跳转到 keycloak统一登录,登录完成之后,再在本地项目进行登录。
明白这个原理之后,其他类型的项目都是一样的处理逻辑。
具体接入时,参见这个文档 securing apps guide,说得很清楚。
另外,参考其GitHub上的 Quick-Start Demo。
附 Keycloak官网: www.keycloak.org
Special专题:关于现代化前端、移动端的接入说明
参见这篇文章:Keycloak现代化前端、移动端的接入说明。