feat: 更新 PigxTokenEndpoint 和相关模板,优化授权流程和界面

This commit is contained in:
冷冷 committed 2025-02-18 22:17:42 +08:00
1 parent cfae5e1c05
commit 7f8626e04f
10 files changed
+414 -354

No files matched your search

@@ -72,184 +72,195 @@ import java.util.stream.Collectors;
*/
@Slf4j
@RestController
@RequestMapping
@RequiredArgsConstructor
@RequestMapping("/token")
public class PigxTokenEndpoint {
private final OAuth2AuthorizationService authorizationService;
private final OAuth2AuthorizationService authorizationService;
private final RemoteClientDetailsService clientDetailsService;
private final RemoteClientDetailsService clientDetailsService;
private final RedisTemplate<String, Object> redisTemplate;
private final RedisTemplate<String, Object> redisTemplate;
private final RemoteTenantService tenantService;
private final RemoteTenantService tenantService;
private final KeyStrResolver tenantKeyStrResolver;
private final KeyStrResolver tenantKeyStrResolver;
private final CacheManager cacheManager;
private final CacheManager cacheManager;
/**
* 认证页面
* @param modelAndView
* @param error 表单登录失败处理回调的错误信息
* @return ModelAndView
*/
@GetMapping("/login")
public ModelAndView require(ModelAndView modelAndView, @RequestParam(required = false) String error) {
modelAndView.setViewName("ftl/login");
modelAndView.addObject("error", error);
/**
* 认证页面
*
* @param modelAndView
* @param error 表单登录失败处理回调的错误信息
* @return ModelAndView
*/
@GetMapping("/token/login")
public ModelAndView require(ModelAndView modelAndView, @RequestParam(required = false) String error) {
modelAndView.setViewName("ftl/login");
modelAndView.addObject("error", error);
R<List<SysTenant>> tenantList = tenantService.list();
modelAndView.addObject("tenantList", tenantList.getData());
return modelAndView;
}
R<List<SysTenant>> tenantList = tenantService.list();
modelAndView.addObject("tenantList", tenantList.getData());
return modelAndView;
}
@GetMapping("/confirm_access")
public ModelAndView confirm(Principal principal, ModelAndView modelAndView,
@RequestParam(OAuth2ParameterNames.CLIENT_ID) String clientId,
@RequestParam(OAuth2ParameterNames.SCOPE) String scope,
@RequestParam(OAuth2ParameterNames.STATE) String state) {
SysOauthClientDetails clientDetails = RetOps
.of(clientDetailsService.getClientDetailsById(clientId))
.getData()
.orElseThrow(() -> new OAuthClientException("clientId 不合法"));
/**
* 授权码模式:确认页面
*
* @return {@link ModelAndView }
*/
@GetMapping("/oauth2/confirm_access")
public ModelAndView confirm(Principal principal, ModelAndView modelAndView,
@RequestParam(OAuth2ParameterNames.CLIENT_ID) String clientId,
@RequestParam(OAuth2ParameterNames.SCOPE) String scope,
@RequestParam(OAuth2ParameterNames.STATE) String state) {
SysOauthClientDetails clientDetails = RetOps.of(clientDetailsService.getClientDetailsById(clientId))
.getData()
.orElseThrow(() -> new OAuthClientException("clientId 不合法"));
Set<String> authorizedScopes = StringUtils.commaDelimitedListToSet(clientDetails.getScope());
modelAndView.addObject("clientId", clientId);
modelAndView.addObject("state", state);
modelAndView.addObject("scopeList", authorizedScopes);
modelAndView.addObject("principalName", principal.getName());
modelAndView.setViewName("ftl/confirm");
return modelAndView;
}
Set<String> authorizedScopes = StringUtils.commaDelimitedListToSet(clientDetails.getScope());
modelAndView.addObject("clientId", clientId);
modelAndView.addObject("state", state);
modelAndView.addObject("scopeList", authorizedScopes);
modelAndView.addObject("principalName", principal.getName());
modelAndView.setViewName("ftl/confirm");
return modelAndView;
}
/**
* 退出并删除token
* @param authHeader Authorization
*/
@DeleteMapping("/logout")
public R<Boolean> logout(@RequestHeader(value = HttpHeaders.AUTHORIZATION, required = false) String authHeader) {
if (StrUtil.isBlank(authHeader)) {
return R.ok();
}
/**
* 注销并删除令牌
*
* @param authHeader auth 标头
* @return {@link R }<{@link Boolean }>
*/
@DeleteMapping("/token/logout")
public R<Boolean> logout(@RequestHeader(value = HttpHeaders.AUTHORIZATION, required = false) String authHeader) {
if (StrUtil.isBlank(authHeader)) {
return R.ok();
}
String tokenValue = authHeader.replace(OAuth2AccessToken.TokenType.BEARER.getValue(), StrUtil.EMPTY).trim();
return removeToken(tokenValue);
}
String tokenValue = authHeader.replace(OAuth2AccessToken.TokenType.BEARER.getValue(), StrUtil.EMPTY).trim();
return removeToken(tokenValue);
}
/**
* 校验token
* @param token 令牌
* @return
*/
@SneakyThrows
@GetMapping("/check_token")
public R<OAuth2AccessToken> checkToken(String token, HttpServletResponse response, HttpServletRequest request) {
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
if (StrUtil.isBlank(token)) {
httpResponse.setStatusCode(HttpStatus.UNAUTHORIZED);
return R.failed(OAuth2ErrorCodesExpand.TOKEN_MISSING);
}
OAuth2Authorization authorization = authorizationService.findByToken(token, OAuth2TokenType.ACCESS_TOKEN);
/**
* 校验token
*
* @param token 令牌
* @return
*/
@SneakyThrows
@GetMapping("/token/check_token")
public R<OAuth2AccessToken> checkToken(String token, HttpServletResponse response, HttpServletRequest request) {
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
// 如果令牌不存在 返回401
if (authorization == null || authorization.getAccessToken() == null) {
httpResponse.setStatusCode(HttpStatus.UNAUTHORIZED);
return R.failed(OAuth2ErrorCodesExpand.INVALID_BEARER_TOKEN);
}
if (StrUtil.isBlank(token)) {
httpResponse.setStatusCode(HttpStatus.UNAUTHORIZED);
return R.failed(OAuth2ErrorCodesExpand.TOKEN_MISSING);
}
OAuth2Authorization authorization = authorizationService.findByToken(token, OAuth2TokenType.ACCESS_TOKEN);
// 获取令牌
return R.ok(Objects.requireNonNull(authorization).getAccessToken().getToken());
}
// 如果令牌不存在 返回401
if (authorization == null || authorization.getAccessToken() == null) {
httpResponse.setStatusCode(HttpStatus.UNAUTHORIZED);
return R.failed(OAuth2ErrorCodesExpand.INVALID_BEARER_TOKEN);
}
/**
* 令牌管理调用
* @param token token
*/
@Inner
@DeleteMapping("/{token}")
public R<Boolean> removeToken(@PathVariable("token") String token) {
OAuth2Authorization authorization = authorizationService.findByToken(token, OAuth2TokenType.ACCESS_TOKEN);
if (authorization == null) {
return R.ok();
}
// 获取令牌
return R.ok(Objects.requireNonNull(authorization).getAccessToken().getToken());
}
OAuth2Authorization.Token<OAuth2AccessToken> accessToken = authorization.getAccessToken();
if (accessToken == null || StrUtil.isBlank(accessToken.getToken().getTokenValue())) {
return R.ok();
}
// 清空用户信息
cacheManager.getCache(CacheConstants.USER_DETAILS).evict(authorization.getPrincipalName());
// 清空access token
authorizationService.remove(authorization);
// 处理自定义退出事件,保存相关日志
SpringContextHolder.publishEvent(new LogoutSuccessEvent(new PreAuthenticatedAuthenticationToken(
authorization.getPrincipalName(), authorization.getRegisteredClientId())));
return R.ok();
}
/**
* 令牌管理调用
*
* @param token token
*/
@Inner
@DeleteMapping("/token/remove/{token}")
public R<Boolean> removeToken(@PathVariable("token") String token) {
OAuth2Authorization authorization = authorizationService.findByToken(token, OAuth2TokenType.ACCESS_TOKEN);
if (authorization == null) {
return R.ok();
}
/**
* 查询token
* @param params 分页参数
* @return
*/
@Inner
@PostMapping("/page")
public R<Page<TokenVO>> tokenList(@RequestBody Map<String, Object> params) {
// 根据分页参数获取对应数据
String key = String.format("%s::%s::*", tenantKeyStrResolver.key(), CacheConstants.PROJECT_OAUTH_ACCESS);
int current = MapUtil.getInt(params, CommonConstants.CURRENT);
int size = MapUtil.getInt(params, CommonConstants.SIZE);
Set<String> keys = redisTemplate.keys(key);
List<String> pages = keys.stream().skip((current - 1) * size).limit(size).collect(Collectors.toList());
Page<TokenVO> result = new Page(current, size);
OAuth2Authorization.Token<OAuth2AccessToken> accessToken = authorization.getAccessToken();
if (accessToken == null || StrUtil.isBlank(accessToken.getToken().getTokenValue())) {
return R.ok();
}
// 清空用户信息
cacheManager.getCache(CacheConstants.USER_DETAILS).evict(authorization.getPrincipalName());
// 清空access token
authorizationService.remove(authorization);
// 处理自定义退出事件,保存相关日志
SpringContextHolder.publishEvent(new LogoutSuccessEvent(new PreAuthenticatedAuthenticationToken(
authorization.getPrincipalName(), authorization.getRegisteredClientId())));
return R.ok();
}
List<TokenVO> tokenVoList = redisTemplate.opsForValue().multiGet(pages).stream().map(obj -> {
OAuth2Authorization authorization = (OAuth2Authorization) obj;
TokenVO tokenVo = new TokenVO();
tokenVo.setClientId(authorization.getRegisteredClientId());
tokenVo.setId(authorization.getId());
tokenVo.setUsername(authorization.getPrincipalName());
OAuth2Authorization.Token<OAuth2AccessToken> accessToken = authorization.getAccessToken();
tokenVo.setAccessToken(accessToken.getToken().getTokenValue());
/**
* 查询token
*
* @param params 分页参数
* @return
*/
@Inner
@PostMapping("/token/page")
public R<Page<TokenVO>> tokenList(@RequestBody Map<String, Object> params) {
// 根据分页参数获取对应数据
String key = String.format("%s::%s::*", tenantKeyStrResolver.key(), CacheConstants.PROJECT_OAUTH_ACCESS);
int current = MapUtil.getInt(params, CommonConstants.CURRENT);
int size = MapUtil.getInt(params, CommonConstants.SIZE);
Set<String> keys = redisTemplate.keys(key);
List<String> pages = keys.stream().skip((current - 1) * size).limit(size).collect(Collectors.toList());
Page<TokenVO> result = new Page(current, size);
String expiresAt = TemporalAccessorUtil.format(accessToken.getToken().getExpiresAt(),
DatePattern.NORM_DATETIME_PATTERN);
tokenVo.setExpiresAt(expiresAt);
List<TokenVO> tokenVoList = redisTemplate.opsForValue().multiGet(pages).stream().map(obj -> {
OAuth2Authorization authorization = (OAuth2Authorization) obj;
TokenVO tokenVo = new TokenVO();
tokenVo.setClientId(authorization.getRegisteredClientId());
tokenVo.setId(authorization.getId());
tokenVo.setUsername(authorization.getPrincipalName());
OAuth2Authorization.Token<OAuth2AccessToken> accessToken = authorization.getAccessToken();
tokenVo.setAccessToken(accessToken.getToken().getTokenValue());
String issuedAt = TemporalAccessorUtil.format(accessToken.getToken().getIssuedAt(),
DatePattern.NORM_DATETIME_PATTERN);
tokenVo.setIssuedAt(issuedAt);
String expiresAt = TemporalAccessorUtil.format(accessToken.getToken().getExpiresAt(),
DatePattern.NORM_DATETIME_PATTERN);
tokenVo.setExpiresAt(expiresAt);
Map<String, Object> attributes = authorization.getAttributes();
Authentication authentication = (Authentication) attributes.get(Principal.class.getName());
String issuedAt = TemporalAccessorUtil.format(accessToken.getToken().getIssuedAt(),
DatePattern.NORM_DATETIME_PATTERN);
tokenVo.setIssuedAt(issuedAt);
if (Objects.isNull(authentication)) {
return tokenVo;
}
Map<String, Object> attributes = authorization.getAttributes();
Authentication authentication = (Authentication) attributes.get(Principal.class.getName());
PigxUser pigxUser = (PigxUser) authentication.getPrincipal();
tokenVo.setUserId(pigxUser.getId());
return tokenVo;
}).filter(tokenVo -> {
// 根据用户名过滤
String username = MapUtil.getStr(params, SecurityConstants.DETAILS_USERNAME);
if (StrUtil.isBlank(username)) {
return true;
}
return tokenVo.getUsername().contains(username);
}).collect(Collectors.toList());
result.setRecords(tokenVoList);
result.setTotal(keys.size());
return R.ok(result);
}
if (Objects.isNull(authentication)) {
return tokenVo;
}
@Inner
@GetMapping("/query-token")
public R queryToken(String token) {
OAuth2Authorization authorization = authorizationService.findByToken(token, OAuth2TokenType.ACCESS_TOKEN);
return R.ok(authorization);
}
PigxUser pigxUser = (PigxUser) authentication.getPrincipal();
tokenVo.setUserId(pigxUser.getId());
return tokenVo;
}).filter(tokenVo -> {
// 根据用户名过滤
String username = MapUtil.getStr(params, SecurityConstants.DETAILS_USERNAME);
if (StrUtil.isBlank(username)) {
return true;
}
return tokenVo.getUsername().contains(username);
}).collect(Collectors.toList());
result.setRecords(tokenVoList);
result.setTotal(keys.size());
return R.ok(result);
}
@Inner
@GetMapping("/query-token")
public R queryToken(String token) {
OAuth2Authorization authorization = authorizationService.findByToken(token, OAuth2TokenType.ACCESS_TOKEN);
return R.ok(authorization);
}
}
@@ -19,13 +19,14 @@ package com.pig4cloud.pigx.auth.support.handler;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.http.HttpUtil;
import com.pig4cloud.pigx.common.core.util.WebUtils;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author lengleng
@@ -47,9 +48,20 @@ public class FormAuthenticationFailureHandler implements AuthenticationFailureHa
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) {
log.debug("表单登录失败:{}", exception.getLocalizedMessage());
String url = HttpUtil.encodeParams(String.format("/token/login?error=%s", exception.getMessage()),
// 获取当前请求的context-path
String contextPath = request.getContextPath();
// 构建重定向URL,加入context-path
String url = HttpUtil.encodeParams(
String.format("%s/token/login?error=%s", contextPath, exception.getMessage()),
CharsetUtil.CHARSET_UTF_8);
WebUtils.getResponse().sendRedirect(url);
try {
WebUtils.getResponse().sendRedirect(url);
} catch (IOException e) {
log.error("重定向失败", e);
}
}
}
@@ -1,6 +0,0 @@
/*!
* Bootstrap v3.3.7 (http://getbootstrap.com)
* Copyright 2011-2016 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
*//*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{margin:.67em 0;font-size:2em}mark{color:#000;background:#ff0}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{height:0;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{margin:0;font:inherit;color:inherit}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}input{line-height:normal}input[type=checkbox],input[type=radio]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{padding:.35em .625em .75em;margin:0 2px;border:1px solid silver}legend{padding:0;border:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-spacing:0;border-collapse:collapse}td,th{padding:0}/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */@media print{*,:after,:before{color:#000!important;text-shadow:none!important;background:0 0!important;-webkit-box-shadow:none!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="javascript:"]:after,a[href^="#"]:after{content:""}blockquote,pre{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #ddd!important}}@font-face{font-family:'Glyphicons Halflings';src:url(../fonts/glyphicons-halflings-regular.eot);src:url(../fonts/glyphicons-halflings-regular.eot?#iefix) format('embedded-opentype'),url(../fonts/glyphicons-halflings-regular.woff2) format('woff2'),url(../fonts/glyphicons-halflings-regular.woff) format('woff'),url(../fonts/glyphicons-halflings-regular.ttf) format('truetype'),url(../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format('svg')}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\002a"}.glyphicon-plus:before{content:"\002b"}.glyphicon-eur:before,.glyphicon-euro:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\Line truncated
/*# sourceMappingURL=bootstrap.min.css.map */
@@ -1,91 +0,0 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: lengleng (wangiegie@gmail.com)
*/
.sign_body {
padding-top: 40px;
padding-bottom: 40px;
background-color: #eee;
}
.form-signin {
max-width: 330px;
padding: 15px;
margin: 0 auto;
}
.form-margin-top {
margin-top: 50px;
}
.form-signin .form-signin-heading,
.form-signin .checkbox {
margin-bottom: 10px;
}
.form-signin .checkbox {
font-weight: normal;
}
.form-signin .form-control {
position: relative;
height: auto;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
padding: 10px;
font-size: 16px;
}
.form-signin .form-control:focus {
z-index: 2;
}
.form-signin input[type="email"] {
margin-bottom: -1px;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}
.form-signin input[type="password"] {
margin-bottom: 10px;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
footer{
text-align: center;
position:absolute;
bottom:0;
width:100%;
height:100px;
}
* {
margin: 0;
padding: 0;
}
.input {
transition: border 0.2s ease-in-out;
min-width: 280px
}
.input:focus + .label,
.input:active + .label,
.input.filled + .label {
font-size: .75rem;
transition: all 0.2s ease-out;
top: -0.3rem;
color: #6b7280;
}
.label {
transition: all 0.2s ease-out;
top: 0.4rem;
left: 0;
}
@@ -1,56 +1,55 @@
<!DOCTYPE html>
<html>
<html>
<head>
<meta charset="UTF-8"/>
<meta name="viewport"
content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no"/>
<title>PigX 第三方授权</title>
<link rel="stylesheet" type="text/css" href="/css/bootstrap.min.css"/>
<link rel="stylesheet" type="text/css" href="/css/signin.css"/>
</head>
<#assign content>
<div class="mb-7">
<h3 class="font-semibold text-2xl text-gray-800 dark:text-gray-200 text-center transition-colors">
应用授权确认</h3>
<div class="mt-4 flex items-center justify-center text-sm">
<div class="px-4 py-2 bg-gray-50 dark:bg-gray-800 rounded-full transition-all">
<div class="text-gray-700 dark:text-gray-300">
<#if principalName=="anonymousUser">
<span class="text-gray-500 dark:text-gray-400">未登录用户</span>
<#else>
<a href="https://pig4cloud.com"
class="text-purple-600 dark:text-purple-400 hover:text-purple-700 dark:hover:text-purple-300 font-medium transition-colors">
${principalName}
</a>
</#if>
</div>
</div>
</div>
</div>
<body>
<nav class="navbar navbar-default container-fluid">
<div class="container">
<div class="navbar-header">
<a class="navbar-brand" href="#">开放平台</a>
</div>
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-5">
<p class="navbar-text navbar-right">
<a target="_blank" href="https://pig4cloud.com">技术支持</a>
</p>
<p class="navbar-text navbar-right">
<#if principalName=="anonymousUser">
未登录
<#else>
<a target="_blank" href="https://pig4cloud.com">${principalName}</a>
</#if>
</p>
</div>
</div>
</nav>
<div style="padding-top: 80px;width: 300px; color: #555; margin:0px auto;">
<form id='confirmationForm' name='confirmationForm' action="/oauth2/authorize" method='post'>
<input type="hidden" name="client_id" value="${clientId}">
<input type="hidden" name="state" value="${state}">
<form id='confirmationForm' name='confirmationForm' action="${request.contextPath}/oauth2/authorize" method='post'>
<input type="hidden" name="client_id" value="${clientId}">
<input type="hidden" name="state" value="${state}">
<p>
将获得以下权限:</p>
<ul class="list-group">
<li class="list-group-item"> <span>
<#list scopeList as scope>
<input type="checkbox" checked="checked" name="scope" value="${scope}"/><label>${scope}</label>
</#list>
</ul>
<p class="help-block">授权后表明你已同意 <a>服务协议</a></p>
<button class="btn btn-success pull-right" type="submit" id="write-email-btn">授权</button>
</p>
</form>
</div>
<footer>
<p>support by: pig4cloud.com</p>
<p>email: <a href="mailto:wangiegie@gmail.com">wangiegie@gmail.com</a>.</p>
</footer>
</body>
</html>
<div class="space-y-6">
<div class="mb-4">
<p class="text-gray-700 dark:text-gray-300 mb-3 transition-colors">将获得以下权限:</p>
<div class="space-y-3 bg-gray-50 dark:bg-gray-800 p-4 rounded-lg border border-gray-200 dark:border-gray-700 transition-all">
<#list scopeList as scope>
<div class="flex items-center">
<input type="checkbox" checked="checked" name="scope" value="${scope}"
class="h-4 w-4 text-purple-600 focus:ring-purple-500 dark:focus:ring-purple-400 border-gray-300 dark:border-gray-600 rounded transition-colors">
<label class="ml-3 text-gray-600 dark:text-gray-400 transition-colors">${scope}</label>
</div>
</#list>
</div>
</div>
<div class="text-sm text-gray-500 dark:text-gray-400 mb-4 transition-colors">
授权后表明你已同意
<a href="#"
class="text-purple-600 dark:text-purple-400 hover:text-purple-700 dark:hover:text-purple-300 transition-colors">服务协议</a>
</div>
<div>
<button type="submit" id="write-email-btn"
class="w-full flex justify-center bg-purple-600 hover:bg-purple-700 dark:bg-purple-700 dark:hover:bg-purple-600 text-gray-100 p-3 rounded-lg tracking-wide font-semibold cursor-pointer transition-all duration-300 transform hover:scale-[1.02]">
确认授权
</button>
</div>
</div>
</form>
</#assign>
<#include "layout/base.ftl">
@@ -0,0 +1,124 @@
<!doctype html>
<html class="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><#if title??>${title}<#else>Pig 统一身份认证</#if></title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
colors: {
primary: '#6366f1'
}
}
}
}
</script>
<style>
.dark-mode-toggle {
transition: all 0.3s ease;
}
.dark-mode-toggle:hover {
transform: scale(1.1);
}
.animate-spin-slow {
animation: spin 2s linear infinite;
}
</style>
<#if extraHead??>${extraHead}</#if>
</head>
<body class="min-h-screen">
<!-- 暗黑模式切换按钮 -->
<div class="fixed top-6 right-6 z-50">
<button id="theme-toggle"
class="dark-mode-toggle p-3 rounded-xl bg-purple-100/90 dark:bg-gray-800/90 text-purple-600 dark:text-yellow-300 hover:bg-purple-200 dark:hover:bg-gray-700 focus:outline-none transition-all duration-300 shadow-lg">
<!-- 月亮图标 -->
<svg id="moon-icon" class="w-6 h-6 hidden dark:block" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"></path>
</svg>
<!-- 太阳图标 -->
<svg id="sun-icon" class="w-6 h-6 block dark:hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"></path>
</svg>
</button>
</div>
<!-- 背景渐变 -->
<div class="bg-purple-900 fixed top-0 left-0 bg-gradient-to-b from-gray-900 via-gray-900 to-purple-800 w-full h-full -z-10">
</div>
<div class="relative min-h-screen flex flex-col md:flex-row items-center justify-center px-4 py-12 md:px-8">
<!-- 左侧说明文本 -->
<div class="flex-1 max-w-xl mb-8 md:mb-0 md:mr-12 z-10">
<div class="hidden md:block">
<h2 class="text-3xl font-bold text-white mb-6">统一身份认证平台</h2>
<p class="text-gray-300 text-lg leading-relaxed opacity-90">
为企业提供一套集中式的账号、权限、认证、审计工具,帮助企业打通身份数据孤岛,实现"一个账号、一次认证、多点通行"的效果,强化企业安全体系的同时,提升组织管理效率,助力企业数字化升级转型。
</p>
</div>
</div>
<!-- 右侧内容区 -->
<div class="flex-1 w-full max-w-md z-10">
<div class="p-8 md:p-10 bg-white dark:bg-gray-900 rounded-2xl shadow-2xl backdrop-blur-sm transition-all duration-300">
<#if content??>${content}</#if>
<!-- 版权信息 -->
<div class="mt-8 pt-6 text-center text-gray-400 dark:text-gray-500 text-sm border-t border-gray-100 dark:border-gray-800">
<span>
Copyright © 2021-2025
<a href="#"
class="text-purple-600 dark:text-purple-400 hover:text-purple-700 dark:hover:text-purple-300 transition-colors">PIGCLOUD</a>
</span>
</div>
</div>
</div>
</div>
<!-- 底部波浪装饰 -->
<svg class="fixed bottom-0 left-0 w-full transition-colors -z-5" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 1440 320">
<path class="fill-white dark:fill-gray-900 transition-colors"
d="M0,0L40,42.7C80,85,160,171,240,197.3C320,224,400,192,480,154.7C560,117,640,75,720,74.7C800,75,880,117,960,154.7C1040,192,1120,224,1200,213.3C1280,203,1360,149,1400,122.7L1440,96L1440,320L1400,320C1360,320,1280,320,1200,320C1120,320,1040,320,960,320C880,320,800,320,720,320C640,320,560,320,480,320C400,320,320,320,240,320C160,320,80,320,40,320L0,320Z">
</path>
</svg>
<script>
// 主题切换功能
const themeToggle = document.getElementById('theme-toggle');
const html = document.documentElement;
// 检查本地存储中的主题设置
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
html.classList.add('dark');
} else {
html.classList.remove('dark');
}
// 切换主题
themeToggle.addEventListener('click', () => {
html.classList.toggle('dark');
// 保存主题设置到本地存储
if (html.classList.contains('dark')) {
localStorage.theme = 'dark';
} else {
localStorage.theme = 'light';
}
// 添加点击动画效果
themeToggle.classList.add('animate-spin-slow');
setTimeout(() => {
themeToggle.classList.remove('animate-spin-slow');
}, 300);
});
</script>
</body>
</html>
@@ -1,50 +1,57 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<#-- 内网部署下载相对应放文件放在 resource目录离线加载-->
<script src="https://cdn.tailwindcss.com"></script>
<link href="/css/signin.css" rel="stylesheet">
</head>
<body>
<div class="h-screen bg-white relative flex flex-col space-y-10 justify-center items-center">
<div class="bg-white md:shadow-lg shadow-none rounded p-6 w-96">
<h1 class="text-3xl font-bold leading-normal text-center">统一身份平台</h1>
<form class="space-y-5 mt-5" action="/token/form" method="post">
<input type="hidden" name="client_id" class="form-control" value="pig" placeholder="所属客户端" >
<#assign content>
<div class="mb-8 text-center">
<svg class="w-16 h-16 mx-auto mb-4 text-purple-600 dark:text-purple-400" viewBox="0 0 24 24" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path d="M12 14.5V16.5M7 10.0288C7.47142 10.0288 7.86284 9.63734 7.86284 9.16592C7.86284 8.69449 7.47142 8.30307 7 8.30307C6.52858 8.30307 6.13716 8.69449 6.13716 9.16592C6.13716 9.63734 6.52858 10.0288 7 10.0288ZM17 10.0288C17.4714 10.0288 17.8628 9.63734 17.8628 9.16592C17.8628 8.69449 17.4714 8.30307 17 8.30307C16.5286 8.30307 16.1372 8.69449 16.1372 9.16592C16.1372 9.63734 16.5286 10.0288 17 10.0288ZM12 12.5C13.6569 12.5 15 11.1569 15 9.5C15 7.84315 13.6569 6.5 12 6.5C10.3431 6.5 9 7.84315 9 9.5C9 11.1569 10.3431 12.5 12 12.5Z"
stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 21C16.9706 21 21 16.9706 21 12C21 7.02944 16.9706 3 12 3C7.02944 3 3 7.02944 3 12C3 16.9706 7.02944 21 12 21Z"
stroke="currentColor" stroke-width="1.5"/>
<path d="M17.6972 19.7C16.0993 18.0307 14.125 17 12 17C9.87499 17 7.90072 18.0307 6.30283 19.7"
stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">安全便捷的企业级认证服务</p>
</div>
<div class="mb-4 relative">
<form class="form-signin" action="${request.contextPath}/oauth2/form" method="post">
<input type="hidden" name="client_id" value="pig">
<input type="hidden" name="grant_type" value="password">
<div class="space-y-6">
<div class="mb-4 relative">
<#if tenantList??>
<select class="w-full rounded px-3 border border-gray-500 pt-5 pb-2 focus:outline-none input active:outline-none" placeholder="所属租户" name="TENANT-ID">
<select class="w-full text-sm px-4 py-3 bg-gray-100 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg focus:outline-none focus:border-purple-400 dark:focus:border-purple-500 dark:text-gray-300 transition-colors"
name="TENANT-ID">
<#list tenantList as tenant>
<option value="${tenant.id}">${tenant.name}</option>
<option value="${tenant.id}">${tenant.name}</option>
</#list>
</select>
</select>
</#if>
</div>
<div class="mb-4 relative">
<input id="username" required name="username"
class="w-full rounded px-3 border border-gray-500 pt-5 pb-2 focus:outline-none input active:outline-none"
type="text" autofocus>
<label for="username"
class="label absolute mb-0 -mt-2 pl-3 leading-tighter text-gray-500 text-base mt-2 cursor-text">账号</label>
</div>
<div class="relative flex items-center border border-gray-500 focus:ring focus:border-blue-500 rounded">
<input id="password" required name="password"
class="w-full rounded px-3 pt-5 outline-none pb-2 focus:outline-none active:outline-none input active:border-blue-500"
type="password"/>
<label for="password"
class="label absolute mb-0 -mt-2 pl-3 leading-tighter text-gray-500 text-base mt-2 cursor-text">密码</label>
</div>
<button class="w-full text-center bg-blue-700 hover:bg-blue-900 rounded-full text-white py-3 font-medium">
登 录
</button>
</form>
<#if error??>
<p class="text-sm leading-normal text-red-900">${error?html}</p>
</#if>
</div>
</div>
</body>
</html>
</div>
<div class="">
<input class="w-full text-sm px-4 py-3 bg-gray-100 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg focus:outline-none focus:border-purple-400 dark:focus:border-purple-500 dark:text-gray-300 transition-colors"
type="text" placeholder="账号" name="username" required>
</div>
<div class="relative">
<input placeholder="密码" type="password" name="password" required
class="w-full text-sm px-4 py-3 bg-gray-100 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg focus:outline-none focus:border-purple-400 dark:focus:border-purple-500 dark:text-gray-300 transition-colors">
</div>
<#if error??>
<div class="relative text-center">
<span class="text-red-600 dark:text-red-400">${error}</span>
</div>
</#if>
<div>
<button type="submit"
class="w-full flex justify-center bg-purple-600 hover:bg-purple-700 dark:bg-purple-700 dark:hover:bg-purple-600 text-gray-100 p-3 rounded-lg tracking-wide font-semibold cursor-pointer transition-all duration-300 transform hover:scale-[1.02]">
登 录
</button>
</div>
</div>
</form>
</#assign>
<#include "layout/base.ftl">
+5 -1
View File
@@ -25,6 +25,10 @@ spring:
redis.enabled: false
sentinel:
enabled: false
freemarker: # freemarker 配置,授权码模式页面渲染使用
suffix: .ftl
template-loader-path: classpath:/templates/
request-context-attribute: request
main:
allow-bean-definition-overriding: true
profiles:
@@ -42,7 +46,7 @@ security:
- /swagger-ui.html
- /swagger-ui/**
- /swagger-resources
- /token/check_token
- /token/**
- /error
- /druid/**
- /actuator/**
@@ -227,7 +227,7 @@ public interface SecurityConstants {
/**
* 授权码模式confirm
*/
String CUSTOM_CONSENT_PAGE_URI = "/token/confirm_access";
String CUSTOM_CONSENT_PAGE_URI = "/oauth2/confirm_access";
/**
* {noop} 加密的特征码
@@ -49,7 +49,7 @@ public interface RemoteTokenService {
* @return
*/
@NoToken
@DeleteMapping("/token/{token}")
@DeleteMapping("/token/remove/{token}")
R<Boolean> removeTokenById(@PathVariable("token") String token);
/**