1.前言
有两个项目,需要用keycloak管理做一键登录(登录一个项目,另一个无需登录可直接访问)
项目1:管理系统(主业务)
项目2:文件上传下载(辅助业务)
授权码模式:(5条消息) keycloak“授权码模式”的运行流程(个人理解)_凯尔萨厮的博客-CSDN博客
2.keycloak前端Vue代码实现(授权码模式的①)
参照:
项目1:前端vue引入keycloak代码(访问项目未登录会跳转到keycloak等登录页)
备注:项目1登出后项目2会认证登录失效(刷新页面才会提示失效)
前提:
执行命令:【npm install @dsb-norge/vue-keycloak-js】引入JS
package.json { dependencies:"vue": "vue": "2.6.12", devDependencies:"vue-template-compiler": "2.6.12"} vue版本需要2.6起才支持keycloak.js
import keycloak from '@dsb-norge/vue-keycloak-js'
Vue.use(keycloak, {
init: {
onLoad: 'login-required', // 登录页面认证
checkLoginIframe: false
},
config: {
// url: 'http:localhost:8080/', // keycloak 20.1版本 url
url: 'http:localhost:8080/auth/', // keycloak 15.0版本 url
realm: 'test_realm_1', // keycloak 域
clientId: 'test_client_1' // keycloak 客户端
},
onReady: (keycloak) => {
keycloak.loadUserProfile().success((data) => {
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
})
}
})
登陆后,获取refreshToken
this.$keycloak.refreshToken
退出登录
this.$keycloak.logoutFn()
项目2:前端vue引入keycloak代码(访问项目未登录,手动指定登录到keycloak的登录页)
备注:项目2登出后项目1会认证登录失效(刷新页面才会提示失效)
import keycloak from "@dsb-norge/vue-keycloak-js";
Vue.use(keycloak, {
init: {
onLoad: "check-sso", // 只做验证,没有登录不跳转登录页面
checkLoginIframe: false,
},
config: {
// url: "http://localhost:8080", // keycloak 20.1版本
url: "http://localhost:8080/auth/" // keycloak 15.0版本
realm: "test_realm_2", // 项目2的 域
clientId: "test_client_2",// 项目2的 客户端
},
onReady(keycloak) {
new Vue({
router,
keycloak,
render: (h) => h(App),
}).$mount("#app");
},
});
判断是否做过登录
import Vue from "vue";
if (Vue.prototype.$keycloak.authenticated) {
// 做过认证登录
} else {
// 没做过认证,获取keycloak的登录页面url
const loginUrl = Vue.prototype.$keycloak.createLoginUrl();
// 跳转到keyclaok的登录页面
window.location.replace(loginUrl);
}
项目3:调用API接口访问keycloak(无论登录登出,无法让项目1项目2同时登录登出)
备注:无法一件登录原因(猜测,axios数据异步处理,没有在浏览器生成Cookie)所以浏览器访问项目1项目2都不认为登陆过,这种方式只适用于前端没有token清空下获取keycloak用户信息
前端直接通过API获取
import axios from 'axios'
import qs from 'qs'
const url = 'http://localhost:8080/realms/test_clien_3/protocol/openid-connect/token'
// keycloak 15.0版 8080后/realms前要加 /auth
const params = {
client_id: 'test_clien_3', // keycloak的客户端
username: this.loginForm.username, // 登录用户名
password: this.loginForm.password, // 登录密码
grant_type: 'password' // 固定值
}
const jsonParams = qs.stringify(params) // 参数要做转换
axios.post(url, jsonParams).then(res => {
// keycloak访问成功可获取到用户信息
}).catch((error) => {
// 异常处理
})
3.keycloak后端Springboot代码实现(授权码模式的④、⑤)
项目1:后端代码
application.properties
keyCloak-baseUrl=http://localhost:8080
keyCloak-realms=test_realm_1
keyCloak-client=test_client_1
keyCloak-accessTokenUrl=${keyCloak-baseUrl}/realms/${keyCloak-realms}/protocol/openid-connect/token
keyCloak-userInfoUrl=${keyCloak-baseUrl}/realms/${keyCloak-realms}/protocol/openid-connect/userinfo
# 15.0版keycloak 地址栏/realms前需要加 /auth
keycloak对象类
@Data
public class AuthenticationToken {
String access_token;
String expires_in;
String refresh_expires_in;
String refresh_token;
String token_type;
String session_state;
String scope;
}
用refreshToken刷新accessToken
public String getAccessToken(String refreshToken) {
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("grant_type", "refresh_token");
params.add("refresh_token", refreshToken);
params.add("client_id", PropertiesUtil.client);
HttpHeaders headers = new HttpHeaders();
headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
HttpEntity<MultiValueMap<String, String>> reqEntity = new HttpEntity<>(params, headers);
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<AuthenticationToken> authToken =
restTemplate.exchange(PropertiesUtil.accessTokenUrl,
HttpMethod.POST, reqEntity, AuthenticationToken.class);
return authenticationToken.getBody().getAccess_token();
}
用accessToken取用户信息
public static UserInfo getUserInfo(String accessToken) {
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setBearerAuth(accessToken);
HttpEntity<MultiValueMap<String, String>> reqEntity = new HttpEntity<>(headers);
ResponseEntity<Object> userInfo =
restTemplate.exchange(PropertiesUtil.userInfoUrl, HttpMethod.POST, reqEntity, Object.class);
Map<String, Object> userInfoMap = (Map<String, Object>) userInfo.getBody();
return getUserInfo(userInfoMap);
}
// 解析返回的用户信息
private static UserInfo getUserInfo(Map<String, Object> userInfoMap) {
UserInfo userInfo = new UserInfo();
Set<String> keys = userInfoMap.keySet();
for (String key : keys) {
switch (key) {
case "error":
throw new Exception("");
case "sub":
userInfo.setId((String)keycloakUserInfoMap.get(key));
break;
case "preferred_username":
userInfo.setLoginId((String)keycloakUserInfoMap.get(key));
break;
case "name":
userInfo.setName((String)keycloakUserInfoMap.get(key));
break;
case "email":
userInfo.setEmail((String)keycloakUserInfoMap.get(key));
break;
case "resource_access":
Map<String, Object> clients = (Map<String, Object>)userInfoMap.get(key);
if (clients == null) {
break;
}
Map<String, Object> roles = (Map<String, Object>)clients.get(PropertiesUtil.client);
if (roles == null) {
break;
}
// 用户角色信息(名称)
List<String> userRoles = (List<String>)roles.get("roles");
break;
default:
break;
}
}
return userInfo;
}
项目2:后端代码
同项目1一样,后端都是调用keycloakAPI的形式访问Keycloak
为了避免页面刷新才能识别keycloak用户登出,后端拦截器加了访问Keycloak处理,accessToken访问失败会返回异常code,前端判断异常code返回keycloak登录页面。
4.后端JAVA代码调用keycloak的API操作数据
(1). pom.xml引入keycloak依赖
(2). application.properties追加值
keyCloak-adminName=admin
keyCloak-adminPassword=123456
keycloak.auth-server-url=http://localhost:8080/auth
keycloak.realm=${keyCloak-realms}
keycloak.resource=${keyCloak-client}
(3). 新建操作用户数据的DTO
import lombok.Data;
@Data
public class KeycloakUserDto {
/**
* 账号
*/
private String userCode;
/**
* 密码
*/
private String password;
/**
* 名
*/
private String firstName;
/**
* 姓
*/
private String lastName;
/**
* 邮箱
*/
private String mailAddress;
}
(4). 处理数据的共通类
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.ws.rs.core.Response;
import KeycloakUserDto;
import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder;
import org.keycloak.OAuth2Constants;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.admin.client.KeycloakBuilder;
import org.keycloak.admin.client.resource.ClientsResource;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.admin.client.resource.UsersResource;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
/**
*/
public class KeycloakUtil {
/**
* keycloak登录数据成功返回的code
*/
private static final int CREATE_OK = 201;
/**
* keycloak服务器连接
*/
private static Keycloak server() {
return KeycloakBuilder.builder()
.serverUrl(KeycloakPropertiesUtil.baseUrl)
.realm("master")
.grantType(OAuth2Constants.PASSWORD)
.clientId("admin-cli")
.username(KeycloakPropertiesUtil.adminUserName)
.password(KeycloakPropertiesUtil.adminUserPassword)
.resteasyClient(new ResteasyClientBuilder().connectionPoolSize(1).build())
.build();
}
/**
* Keycloak作成用户
* @param dto 用户情报
* @return 结果 true:成功 false:失败
*/
public static boolean createUser(KeycloakCreateUserDto dto) {
// 开启服务器连接
try (Keycloak kcServer = server()) {
// 作成用户数据格式
UserRepresentation user = editCreateUserInfo(dto);
// 登录用户
Response res = kcServer.realm(KeycloakPropertiesUtil.realms).users().create(user);
关闭服务器
kcServer.close();
// 返回结果
return res.getStatus() == CREATE_OK;
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
/**
* Keycloak为用户赋予客户端权限
* @param userCode 账号
* @param clientRole 客户端权限名
* @return 结果 true:成功 false:失败
*/
public static boolean updateUserClientRole(String userCode, String clientRole) {
// 开启服务器连接
try (Keycloak kcServer = server()) {
RealmResource realm = kcServer.realm(KeycloakPropertiesUtil.realms);
ClientsResource clients = realm.clients();
UsersResource users = realm.users();
// 通过账号取用户(正常只会有一个)
List<UserRepresentation> sameNameClientUser = users.search(userCode);
for (UserRepresentation user : sameNameClientUser) {
// 取客户端
List<ClientRepresentation> sameNameClient = clients.findByClientId(KeycloakPropertiesUtil.client);
for (ClientRepresentation client : sameNameClient) {
// 取客户端权限
RoleRepresentation role = clients.get(client.getId()).roles().get(clientRole).toRepresentation();
// 为用户设定客户端权限
List<RoleRepresentation> roles = new ArrayList<>();
roles.add(role);
users.get(user.getId()).roles().clientLevel(client.getId()).add(roles);
}
}
kcServer.close();
return true;
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
/**
* 变更密码
* @param userCode 账号
* @param password 密码
* @return 结果 true:成功 false:失败
*/
public static boolean updatePassword(String userCode, String password) {
CredentialRepresentation ps = editPasswordInfo(password);
// 连接服务器
try (Keycloak kcServer = server()) {
// 密码变更
kcServer.realm(KeycloakPropertiesUtil.realms).users().get(userCode).resetPassword(ps);
return true;
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
/**
* 删除用户
* @param userCode 账号
* @return 処理結果 true:成功 false:失敗
*/
public static boolean deleteUser(String userCode) {
// 连接服务器
try (Keycloak kcServer = server()) {
UsersResource users = kcServer.realm(KeycloakPropertiesUtil.realms).users();
// 查询用户
List<UserRepresentation> sameNameClientUser = users.search(userCode);
for (UserRepresentation user : sameNameClientUser) {
// 删除用户
users.delete(user.getId());
}
} catch (Exception e) {
return false;
}
return true;
}
/**
* 编辑用户信息
*/
private static UserRepresentation editCreateUserInfo(KeycloakCreateUserDto dto) {
UserRepresentation user = new UserRepresentation();
user.setUsername(dto.getUserCode());
user.setCredentials(Collections.singletonList(editPasswordInfo(dto.getPassword())));
user.setFirstName(dto.getFirstName());
user.setLastName(dto.getLastName());
user.setEmail(dto.getMailAddress());
user.setEnabled(true);
return user;
}
/**
* 编辑密码信息
*/
private static CredentialRepresentation editPasswordInfo(String password) {
CredentialRepresentation ps = new CredentialRepresentation();
ps.setType(CredentialRepresentation.PASSWORD);
ps.setValue(password);
ps.setTemporary(false);
return ps;
}
}