@@ -0,0 +1,18 @@ | |||
FROM pig4cloud/java:8-jre | |||
MAINTAINER wangiegie@gmail.com | |||
ENV TZ=Asia/Shanghai | |||
ENV JAVA_OPTS="-Xms128m -Xmx256m -Djava.security.egd=file:/dev/./urandom" | |||
RUN ln -sf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone | |||
RUN mkdir -p /pigx-auth | |||
WORKDIR /pigx-auth | |||
EXPOSE 3000 | |||
ADD ./target/pigx-auth.jar ./ | |||
CMD sleep 120;java $JAVA_OPTS -jar pigx-auth.jar |
@@ -0,0 +1,129 @@ | |||
<?xml version="1.0" encoding="UTF-8"?> | |||
<!-- | |||
~ | |||
~ 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) | |||
~ | |||
--> | |||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0" | |||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> | |||
<modelVersion>4.0.0</modelVersion> | |||
<parent> | |||
<groupId>com.pig4cloud</groupId> | |||
<artifactId>pigx</artifactId> | |||
<version>3.9.0</version> | |||
</parent> | |||
<artifactId>pigx-auth</artifactId> | |||
<packaging>jar</packaging> | |||
<description>pigx 认证授权中心,基于 spring security oAuth2</description> | |||
<dependencies> | |||
<!--注册中心客户端--> | |||
<dependency> | |||
<groupId>com.alibaba.cloud</groupId> | |||
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> | |||
</dependency> | |||
<!--配置中心客户端--> | |||
<dependency> | |||
<groupId>com.alibaba.cloud</groupId> | |||
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> | |||
</dependency> | |||
<!--upms api、model 模块--> | |||
<dependency> | |||
<groupId>com.pig4cloud</groupId> | |||
<artifactId>pigx-upms-api</artifactId> | |||
</dependency> | |||
<!--log--> | |||
<dependency> | |||
<groupId>com.pig4cloud</groupId> | |||
<artifactId>pigx-common-log</artifactId> | |||
</dependency> | |||
<!--security--> | |||
<dependency> | |||
<groupId>com.pig4cloud</groupId> | |||
<artifactId>pigx-common-security</artifactId> | |||
</dependency> | |||
<!--feign 依赖--> | |||
<dependency> | |||
<groupId>com.pig4cloud</groupId> | |||
<artifactId>pigx-common-feign</artifactId> | |||
</dependency> | |||
<!--mysql 驱动--> | |||
<dependency> | |||
<groupId>mysql</groupId> | |||
<artifactId>mysql-connector-java</artifactId> | |||
</dependency> | |||
<!--缓存操作--> | |||
<dependency> | |||
<groupId>com.pig4cloud</groupId> | |||
<artifactId>pigx-common-data</artifactId> | |||
</dependency> | |||
<!--sentinel 依赖--> | |||
<dependency> | |||
<groupId>com.pig4cloud</groupId> | |||
<artifactId>pigx-common-sentinel</artifactId> | |||
</dependency> | |||
<!--路由控制--> | |||
<dependency> | |||
<groupId>com.pig4cloud</groupId> | |||
<artifactId>pigx-common-gray</artifactId> | |||
</dependency> | |||
<!--JDBC相关--> | |||
<dependency> | |||
<groupId>org.springframework.boot</groupId> | |||
<artifactId>spring-boot-starter-jdbc</artifactId> | |||
</dependency> | |||
<!-- druid 连接池 --> | |||
<dependency> | |||
<groupId>com.alibaba</groupId> | |||
<artifactId>druid-spring-boot-starter</artifactId> | |||
</dependency> | |||
<!--freemarker--> | |||
<dependency> | |||
<groupId>org.springframework.boot</groupId> | |||
<artifactId>spring-boot-starter-freemarker</artifactId> | |||
</dependency> | |||
<!--web 模块--> | |||
<dependency> | |||
<groupId>org.springframework.boot</groupId> | |||
<artifactId>spring-boot-starter-web</artifactId> | |||
</dependency> | |||
<!--undertow容器--> | |||
<dependency> | |||
<groupId>org.springframework.boot</groupId> | |||
<artifactId>spring-boot-starter-undertow</artifactId> | |||
</dependency> | |||
</dependencies> | |||
<build> | |||
<plugins> | |||
<plugin> | |||
<groupId>org.springframework.boot</groupId> | |||
<artifactId>spring-boot-maven-plugin</artifactId> | |||
</plugin> | |||
<plugin> | |||
<groupId>io.fabric8</groupId> | |||
<artifactId>docker-maven-plugin</artifactId> | |||
<configuration> | |||
<skip>false</skip> | |||
</configuration> | |||
</plugin> | |||
</plugins> | |||
</build> | |||
</project> |
@@ -0,0 +1,38 @@ | |||
/* | |||
* | |||
* 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) | |||
* | |||
*/ | |||
package com.pig4cloud.pigx.auth; | |||
import com.pig4cloud.pigx.common.feign.annotation.EnablePigxFeignClients; | |||
import org.springframework.boot.SpringApplication; | |||
import org.springframework.cloud.client.SpringCloudApplication; | |||
/** | |||
* @author lengleng | |||
* @date 2020-02-08 认证授权中心 | |||
*/ | |||
@SpringCloudApplication | |||
@EnablePigxFeignClients | |||
public class PigxAuthApplication { | |||
public static void main(String[] args) { | |||
SpringApplication.run(PigxAuthApplication.class, args); | |||
} | |||
} |
@@ -0,0 +1,98 @@ | |||
/* | |||
* | |||
* 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) | |||
* | |||
*/ | |||
package com.pig4cloud.pigx.auth.config; | |||
import cn.hutool.core.util.StrUtil; | |||
import com.pig4cloud.pigx.common.core.constant.SecurityConstants; | |||
import com.pig4cloud.pigx.common.data.tenant.TenantContextHolder; | |||
import com.pig4cloud.pigx.common.security.component.PigxWebResponseExceptionTranslator; | |||
import lombok.AllArgsConstructor; | |||
import lombok.SneakyThrows; | |||
import org.springframework.context.annotation.Bean; | |||
import org.springframework.context.annotation.Configuration; | |||
import org.springframework.data.redis.connection.RedisConnectionFactory; | |||
import org.springframework.http.HttpMethod; | |||
import org.springframework.security.authentication.AuthenticationManager; | |||
import org.springframework.security.core.userdetails.UserDetailsService; | |||
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; | |||
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; | |||
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; | |||
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; | |||
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; | |||
import org.springframework.security.oauth2.provider.ClientDetailsService; | |||
import org.springframework.security.oauth2.provider.OAuth2Authentication; | |||
import org.springframework.security.oauth2.provider.token.DefaultAuthenticationKeyGenerator; | |||
import org.springframework.security.oauth2.provider.token.TokenEnhancer; | |||
import org.springframework.security.oauth2.provider.token.TokenStore; | |||
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore; | |||
/** | |||
* @author lengleng | |||
* @date 2018/6/22 认证服务器配置 | |||
*/ | |||
@Configuration | |||
@AllArgsConstructor | |||
@EnableAuthorizationServer | |||
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { | |||
private final ClientDetailsService pigxClientDetailsServiceImpl; | |||
private final AuthenticationManager authenticationManagerBean; | |||
private final RedisConnectionFactory redisConnectionFactory; | |||
private final UserDetailsService pigxUserDetailsService; | |||
private final TokenEnhancer pigxTokenEnhancer; | |||
@Override | |||
@SneakyThrows | |||
public void configure(ClientDetailsServiceConfigurer clients) { | |||
clients.withClientDetails(pigxClientDetailsServiceImpl); | |||
} | |||
@Override | |||
public void configure(AuthorizationServerSecurityConfigurer oauthServer) { | |||
oauthServer.allowFormAuthenticationForClients().checkTokenAccess("isAuthenticated()"); | |||
} | |||
@Override | |||
public void configure(AuthorizationServerEndpointsConfigurer endpoints) { | |||
endpoints.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST).tokenStore(tokenStore()) | |||
.tokenEnhancer(pigxTokenEnhancer).userDetailsService(pigxUserDetailsService) | |||
.authenticationManager(authenticationManagerBean).reuseRefreshTokens(false) | |||
.pathMapping("/oauth/confirm_access", "/token/confirm_access") | |||
.exceptionTranslator(new PigxWebResponseExceptionTranslator()); | |||
} | |||
@Bean | |||
public TokenStore tokenStore() { | |||
RedisTokenStore tokenStore = new RedisTokenStore(redisConnectionFactory); | |||
tokenStore.setPrefix(SecurityConstants.PIGX_PREFIX + SecurityConstants.OAUTH_PREFIX); | |||
tokenStore.setAuthenticationKeyGenerator(new DefaultAuthenticationKeyGenerator() { | |||
@Override | |||
public String extractKey(OAuth2Authentication authentication) { | |||
return super.extractKey(authentication) + StrUtil.COLON + TenantContextHolder.getTenantId(); | |||
} | |||
}); | |||
return tokenStore; | |||
} | |||
} |
@@ -0,0 +1,103 @@ | |||
/* | |||
* | |||
* 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) | |||
* | |||
*/ | |||
package com.pig4cloud.pigx.auth.config; | |||
import com.pig4cloud.pigx.common.security.handler.FormAuthenticationFailureHandler; | |||
import com.pig4cloud.pigx.common.security.handler.MobileLoginSuccessHandler; | |||
import com.pig4cloud.pigx.common.security.mobile.MobileSecurityConfigurer; | |||
import lombok.SneakyThrows; | |||
import org.springframework.context.annotation.Bean; | |||
import org.springframework.context.annotation.Configuration; | |||
import org.springframework.context.annotation.Primary; | |||
import org.springframework.core.annotation.Order; | |||
import org.springframework.http.HttpHeaders; | |||
import org.springframework.security.authentication.AuthenticationManager; | |||
import org.springframework.security.config.annotation.web.builders.HttpSecurity; | |||
import org.springframework.security.config.annotation.web.builders.WebSecurity; | |||
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; | |||
import org.springframework.security.crypto.factory.PasswordEncoderFactories; | |||
import org.springframework.security.crypto.password.PasswordEncoder; | |||
import org.springframework.security.web.authentication.AuthenticationFailureHandler; | |||
import org.springframework.security.web.authentication.AuthenticationSuccessHandler; | |||
/** | |||
* @author lengleng | |||
* @date 2018/6/22 认证相关配置 | |||
*/ | |||
@Primary | |||
@Order(90) | |||
@Configuration | |||
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter { | |||
@Override | |||
@SneakyThrows | |||
protected void configure(HttpSecurity http) { | |||
http.formLogin().loginPage("/token/login").loginProcessingUrl("/token/form") | |||
.failureHandler(authenticationFailureHandler()).and().logout() | |||
.logoutSuccessHandler((request, response, authentication) -> { | |||
String referer = request.getHeader(HttpHeaders.REFERER); | |||
response.sendRedirect(referer); | |||
}).deleteCookies("JSESSIONID").invalidateHttpSession(true).and().authorizeRequests() | |||
.antMatchers("/token/**", "/actuator/**", "/mobile/**").permitAll().anyRequest().authenticated().and() | |||
.csrf().disable().apply(mobileSecurityConfigurer()); | |||
} | |||
/** | |||
* 不拦截静态资源 | |||
* @param web | |||
*/ | |||
@Override | |||
public void configure(WebSecurity web) { | |||
web.ignoring().antMatchers("/css/**"); | |||
} | |||
@Bean | |||
@Override | |||
@SneakyThrows | |||
public AuthenticationManager authenticationManagerBean() { | |||
return super.authenticationManagerBean(); | |||
} | |||
@Bean | |||
public AuthenticationFailureHandler authenticationFailureHandler() { | |||
return new FormAuthenticationFailureHandler(); | |||
} | |||
@Bean | |||
public AuthenticationSuccessHandler mobileLoginSuccessHandler() { | |||
return new MobileLoginSuccessHandler(); | |||
} | |||
@Bean | |||
public MobileSecurityConfigurer mobileSecurityConfigurer() { | |||
return new MobileSecurityConfigurer(); | |||
} | |||
/** | |||
* https://spring.io/blog/2017/11/01/spring-security-5-0-0-rc1-released#password-storage-updated | |||
* Encoded password does not look like BCrypt | |||
* @return PasswordEncoder | |||
*/ | |||
@Bean | |||
public PasswordEncoder passwordEncoder() { | |||
return PasswordEncoderFactories.createDelegatingPasswordEncoder(); | |||
} | |||
} |
@@ -0,0 +1,213 @@ | |||
/* | |||
* | |||
* 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) | |||
* | |||
*/ | |||
package com.pig4cloud.pigx.auth.endpoint; | |||
import cn.hutool.core.map.MapUtil; | |||
import cn.hutool.core.util.StrUtil; | |||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; | |||
import com.pig4cloud.pigx.common.core.constant.CacheConstants; | |||
import com.pig4cloud.pigx.common.core.constant.PaginationConstants; | |||
import com.pig4cloud.pigx.common.core.constant.SecurityConstants; | |||
import com.pig4cloud.pigx.common.core.util.R; | |||
import com.pig4cloud.pigx.common.data.tenant.TenantContextHolder; | |||
import com.pig4cloud.pigx.common.security.annotation.Inner; | |||
import com.pig4cloud.pigx.common.security.util.SecurityUtils; | |||
import lombok.AllArgsConstructor; | |||
import lombok.extern.slf4j.Slf4j; | |||
import org.springframework.cache.CacheManager; | |||
import org.springframework.data.redis.core.ConvertingCursor; | |||
import org.springframework.data.redis.core.Cursor; | |||
import org.springframework.data.redis.core.RedisTemplate; | |||
import org.springframework.data.redis.core.ScanOptions; | |||
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer; | |||
import org.springframework.data.redis.serializer.RedisSerializer; | |||
import org.springframework.data.redis.serializer.StringRedisSerializer; | |||
import org.springframework.http.HttpHeaders; | |||
import org.springframework.security.oauth2.common.OAuth2AccessToken; | |||
import org.springframework.security.oauth2.common.OAuth2RefreshToken; | |||
import org.springframework.security.oauth2.provider.AuthorizationRequest; | |||
import org.springframework.security.oauth2.provider.ClientDetails; | |||
import org.springframework.security.oauth2.provider.ClientDetailsService; | |||
import org.springframework.security.oauth2.provider.OAuth2Authentication; | |||
import org.springframework.security.oauth2.provider.token.TokenStore; | |||
import org.springframework.web.bind.annotation.*; | |||
import org.springframework.web.servlet.ModelAndView; | |||
import javax.servlet.http.HttpServletRequest; | |||
import javax.servlet.http.HttpSession; | |||
import java.io.IOException; | |||
import java.util.ArrayList; | |||
import java.util.List; | |||
import java.util.Map; | |||
/** | |||
* @author lengleng | |||
* @date 2018/6/24 删除token端点 | |||
*/ | |||
@Slf4j | |||
@RestController | |||
@AllArgsConstructor | |||
@RequestMapping("/token") | |||
public class PigxTokenEndpoint { | |||
private static final String PIGX_OAUTH_ACCESS = SecurityConstants.PIGX_PREFIX + SecurityConstants.OAUTH_PREFIX | |||
+ "auth_to_access:"; | |||
private final ClientDetailsService clientDetailsService; | |||
private final RedisTemplate redisTemplate; | |||
private final TokenStore tokenStore; | |||
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); | |||
return modelAndView; | |||
} | |||
/** | |||
* 确认授权页面 | |||
* @param request | |||
* @param session | |||
* @param modelAndView | |||
* @return | |||
*/ | |||
@GetMapping("/confirm_access") | |||
public ModelAndView confirm(HttpServletRequest request, HttpSession session, ModelAndView modelAndView) { | |||
Map<String, Object> scopeList = (Map<String, Object>) request.getAttribute("scopes"); | |||
modelAndView.addObject("scopeList", scopeList.keySet()); | |||
Object auth = session.getAttribute("authorizationRequest"); | |||
if (auth != null) { | |||
AuthorizationRequest authorizationRequest = (AuthorizationRequest) auth; | |||
ClientDetails clientDetails = clientDetailsService.loadClientByClientId(authorizationRequest.getClientId()); | |||
modelAndView.addObject("app", clientDetails.getAdditionalInformation()); | |||
modelAndView.addObject("user", SecurityUtils.getUser()); | |||
} | |||
modelAndView.setViewName("ftl/confirm"); | |||
return modelAndView; | |||
} | |||
/** | |||
* 退出token | |||
* @param authHeader Authorization | |||
*/ | |||
@DeleteMapping("/logout") | |||
public R logout(@RequestHeader(value = HttpHeaders.AUTHORIZATION, required = false) String authHeader) { | |||
if (StrUtil.isBlank(authHeader)) { | |||
return R.ok(Boolean.FALSE, "退出失败,token 为空"); | |||
} | |||
String tokenValue = authHeader.replace(OAuth2AccessToken.BEARER_TYPE, StrUtil.EMPTY).trim(); | |||
return delToken(tokenValue); | |||
} | |||
/** | |||
* 令牌管理调用 | |||
* @param token token | |||
* @return | |||
*/ | |||
@Inner | |||
@DeleteMapping("/{token}") | |||
public R<Boolean> delToken(@PathVariable("token") String token) { | |||
OAuth2AccessToken accessToken = tokenStore.readAccessToken(token); | |||
if (accessToken == null || StrUtil.isBlank(accessToken.getValue())) { | |||
return R.ok(Boolean.TRUE, "退出失败,token 无效"); | |||
} | |||
OAuth2Authentication auth2Authentication = tokenStore.readAuthentication(accessToken); | |||
// 清空用户信息 | |||
cacheManager.getCache(CacheConstants.USER_DETAILS).evict(auth2Authentication.getName()); | |||
// 清空access token | |||
tokenStore.removeAccessToken(accessToken); | |||
// 清空 refresh token | |||
OAuth2RefreshToken refreshToken = accessToken.getRefreshToken(); | |||
tokenStore.removeRefreshToken(refreshToken); | |||
return R.ok(); | |||
} | |||
/** | |||
* 查询token | |||
* @param params 分页参数 | |||
* @return | |||
*/ | |||
@Inner | |||
@PostMapping("/page") | |||
public R<Page> tokenList(@RequestBody Map<String, Object> params) { | |||
// 根据分页参数获取对应数据 | |||
String key = String.format("%s*:%s", PIGX_OAUTH_ACCESS, TenantContextHolder.getTenantId()); | |||
List<String> pages = findKeysForPage(key, MapUtil.getInt(params, PaginationConstants.CURRENT), | |||
MapUtil.getInt(params, PaginationConstants.SIZE)); | |||
redisTemplate.setKeySerializer(new StringRedisSerializer()); | |||
redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer()); | |||
Page result = new Page(MapUtil.getInt(params, PaginationConstants.CURRENT), | |||
MapUtil.getInt(params, PaginationConstants.SIZE)); | |||
result.setRecords(redisTemplate.opsForValue().multiGet(pages)); | |||
result.setTotal(redisTemplate.keys(key).size()); | |||
return R.ok(result); | |||
} | |||
private List<String> findKeysForPage(String patternKey, int pageNum, int pageSize) { | |||
ScanOptions options = ScanOptions.scanOptions().count(1000L).match(patternKey).build(); | |||
RedisSerializer<String> redisSerializer = (RedisSerializer<String>) redisTemplate.getKeySerializer(); | |||
Cursor cursor = (Cursor) redisTemplate.executeWithStickyConnection( | |||
redisConnection -> new ConvertingCursor<>(redisConnection.scan(options), redisSerializer::deserialize)); | |||
List<String> result = new ArrayList<>(); | |||
int tmpIndex = 0; | |||
int startIndex = (pageNum - 1) * pageSize; | |||
int end = pageNum * pageSize; | |||
assert cursor != null; | |||
while (cursor.hasNext()) { | |||
if (tmpIndex >= startIndex && tmpIndex < end) { | |||
result.add(cursor.next().toString()); | |||
tmpIndex++; | |||
continue; | |||
} | |||
if (tmpIndex >= end) { | |||
break; | |||
} | |||
tmpIndex++; | |||
cursor.next(); | |||
} | |||
try { | |||
cursor.close(); | |||
} | |||
catch (IOException e) { | |||
log.error("关闭cursor 失败"); | |||
} | |||
return result; | |||
} | |||
} |
@@ -0,0 +1,77 @@ | |||
/* | |||
* 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) | |||
*/ | |||
package com.pig4cloud.pigx.auth.handler; | |||
import com.pig4cloud.pigx.admin.api.entity.SysLog; | |||
import com.pig4cloud.pigx.admin.api.feign.RemoteLogService; | |||
import com.pig4cloud.pigx.common.core.constant.CommonConstants; | |||
import com.pig4cloud.pigx.common.core.constant.SecurityConstants; | |||
import com.pig4cloud.pigx.common.core.util.WebUtils; | |||
import com.pig4cloud.pigx.common.log.util.SysLogUtils; | |||
import com.pig4cloud.pigx.common.security.handler.AuthenticationFailureHandler; | |||
import lombok.AllArgsConstructor; | |||
import lombok.SneakyThrows; | |||
import lombok.extern.slf4j.Slf4j; | |||
import org.springframework.http.HttpHeaders; | |||
import org.springframework.scheduling.annotation.Async; | |||
import org.springframework.security.core.Authentication; | |||
import org.springframework.security.core.AuthenticationException; | |||
import org.springframework.stereotype.Component; | |||
import javax.servlet.http.HttpServletRequest; | |||
import javax.servlet.http.HttpServletResponse; | |||
/** | |||
* @author lengleng | |||
* @date 2018/10/8 | |||
*/ | |||
@Slf4j | |||
@Component | |||
@AllArgsConstructor | |||
public class PigxAuthenticationFailureEventHandler implements AuthenticationFailureHandler { | |||
private final RemoteLogService logService; | |||
/** | |||
* 异步处理,登录失败方法 | |||
* <p> | |||
* @param authenticationException 登录的authentication 对象 | |||
* @param authentication 登录的authenticationException 对象 | |||
* @param request 请求 | |||
* @param response 响应 | |||
*/ | |||
@Async | |||
@Override | |||
@SneakyThrows | |||
public void handle(AuthenticationException authenticationException, Authentication authentication, | |||
HttpServletRequest request, HttpServletResponse response) { | |||
String username = authentication.getName(); | |||
SysLog sysLog = SysLogUtils.getSysLog(request, username); | |||
sysLog.setTitle(username + "用户登录"); | |||
sysLog.setType(CommonConstants.STATUS_LOCK); | |||
sysLog.setParams(username); | |||
sysLog.setException(authenticationException.getLocalizedMessage()); | |||
String header = request.getHeader(HttpHeaders.AUTHORIZATION); | |||
sysLog.setServiceId(WebUtils.extractClientId(header).orElse("N/A")); | |||
logService.saveLog(sysLog, SecurityConstants.FROM_IN); | |||
log.info("用户:{} 登录失败,异常:{}", username, authenticationException.getLocalizedMessage()); | |||
} | |||
} |
@@ -0,0 +1,69 @@ | |||
/* | |||
* 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) | |||
*/ | |||
package com.pig4cloud.pigx.auth.handler; | |||
import com.pig4cloud.pigx.admin.api.entity.SysLog; | |||
import com.pig4cloud.pigx.admin.api.feign.RemoteLogService; | |||
import com.pig4cloud.pigx.common.core.constant.SecurityConstants; | |||
import com.pig4cloud.pigx.common.core.util.WebUtils; | |||
import com.pig4cloud.pigx.common.log.util.SysLogUtils; | |||
import com.pig4cloud.pigx.common.security.handler.AuthenticationSuccessHandler; | |||
import lombok.AllArgsConstructor; | |||
import lombok.extern.slf4j.Slf4j; | |||
import org.springframework.http.HttpHeaders; | |||
import org.springframework.scheduling.annotation.Async; | |||
import org.springframework.security.core.Authentication; | |||
import org.springframework.stereotype.Component; | |||
import javax.servlet.http.HttpServletRequest; | |||
import javax.servlet.http.HttpServletResponse; | |||
/** | |||
* @author lengleng | |||
* @date 2018/10/8 | |||
*/ | |||
@Slf4j | |||
@Component | |||
@AllArgsConstructor | |||
public class PigxAuthenticationSuccessEventHandler implements AuthenticationSuccessHandler { | |||
private final RemoteLogService logService; | |||
/** | |||
* 处理登录成功方法 | |||
* <p> | |||
* 获取到登录的authentication 对象 | |||
* @param authentication 登录对象 | |||
* @param request 请求 | |||
* @param response 返回 | |||
*/ | |||
@Async | |||
@Override | |||
public void handle(Authentication authentication, HttpServletRequest request, HttpServletResponse response) { | |||
String username = authentication.getName(); | |||
SysLog sysLog = SysLogUtils.getSysLog(request, username); | |||
sysLog.setTitle(username + "用户登录"); | |||
sysLog.setParams(username); | |||
String header = request.getHeader(HttpHeaders.AUTHORIZATION); | |||
sysLog.setServiceId(WebUtils.extractClientId(header).orElse("N/A")); | |||
logService.saveLog(sysLog, SecurityConstants.FROM_IN); | |||
log.info("用户:{} 登录成功", username); | |||
} | |||
} |
@@ -0,0 +1,41 @@ | |||
package com.pig4cloud.pigx.auth.service; | |||
import com.pig4cloud.pigx.common.core.constant.CacheConstants; | |||
import com.pig4cloud.pigx.common.core.constant.SecurityConstants; | |||
import com.pig4cloud.pigx.common.data.tenant.TenantContextHolder; | |||
import org.springframework.cache.annotation.Cacheable; | |||
import org.springframework.security.oauth2.common.exceptions.InvalidClientException; | |||
import org.springframework.security.oauth2.provider.ClientDetails; | |||
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService; | |||
import org.springframework.stereotype.Service; | |||
import javax.sql.DataSource; | |||
/** | |||
* @author lengleng | |||
* @date 2020/03/25 | |||
* <p> | |||
* 扩展 JdbcClientDetailsService 支持多租户 | |||
*/ | |||
@Service | |||
public class PigxClientDetailsServiceImpl extends JdbcClientDetailsService { | |||
public PigxClientDetailsServiceImpl(DataSource dataSource) { | |||
super(dataSource); | |||
} | |||
/** | |||
* 重写原生方法支持redis缓存 | |||
* @param clientId | |||
* @return ClientDetails | |||
* @throws InvalidClientException | |||
*/ | |||
@Override | |||
@Cacheable(value = CacheConstants.CLIENT_DETAILS_KEY, key = "#clientId", unless = "#result == null") | |||
public ClientDetails loadClientByClientId(String clientId) { | |||
super.setSelectClientDetailsSql( | |||
String.format(SecurityConstants.DEFAULT_SELECT_STATEMENT, TenantContextHolder.getTenantId())); | |||
return super.loadClientByClientId(clientId); | |||
} | |||
} |
@@ -0,0 +1,123 @@ | |||
/* | |||
* 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) | |||
*/ | |||
package com.pig4cloud.pigx.auth.service; | |||
import cn.hutool.core.util.ArrayUtil; | |||
import cn.hutool.core.util.StrUtil; | |||
import com.pig4cloud.pigx.admin.api.dto.UserInfo; | |||
import com.pig4cloud.pigx.admin.api.entity.SysUser; | |||
import com.pig4cloud.pigx.admin.api.feign.RemoteUserService; | |||
import com.pig4cloud.pigx.common.core.constant.CacheConstants; | |||
import com.pig4cloud.pigx.common.core.constant.CommonConstants; | |||
import com.pig4cloud.pigx.common.core.constant.SecurityConstants; | |||
import com.pig4cloud.pigx.common.core.util.R; | |||
import com.pig4cloud.pigx.common.security.service.PigxUser; | |||
import com.pig4cloud.pigx.common.security.service.PigxUserDetailsService; | |||
import lombok.AllArgsConstructor; | |||
import lombok.SneakyThrows; | |||
import lombok.extern.slf4j.Slf4j; | |||
import org.springframework.cache.Cache; | |||
import org.springframework.cache.CacheManager; | |||
import org.springframework.security.core.GrantedAuthority; | |||
import org.springframework.security.core.authority.AuthorityUtils; | |||
import org.springframework.security.core.userdetails.UserDetails; | |||
import org.springframework.security.core.userdetails.UsernameNotFoundException; | |||
import org.springframework.stereotype.Service; | |||
import java.util.Arrays; | |||
import java.util.Collection; | |||
import java.util.HashSet; | |||
import java.util.Set; | |||
/** | |||
* 用户详细信息 | |||
* | |||
* @author lengleng | |||
*/ | |||
@Slf4j | |||
@Service | |||
@AllArgsConstructor | |||
public class PigxUserDetailsServiceImpl implements PigxUserDetailsService { | |||
private final RemoteUserService remoteUserService; | |||
private final CacheManager cacheManager; | |||
/** | |||
* 用户密码登录 | |||
* @param username 用户名 | |||
* @return | |||
* @throws UsernameNotFoundException | |||
*/ | |||
@Override | |||
@SneakyThrows | |||
public UserDetails loadUserByUsername(String username) { | |||
Cache cache = cacheManager.getCache(CacheConstants.USER_DETAILS); | |||
if (cache != null && cache.get(username) != null) { | |||
return (PigxUser) cache.get(username).get(); | |||
} | |||
R<UserInfo> result = remoteUserService.info(username, SecurityConstants.FROM_IN); | |||
UserDetails userDetails = getUserDetails(result); | |||
cache.put(username, userDetails); | |||
return userDetails; | |||
} | |||
/** | |||
* 根据社交登录code 登录 | |||
* @param inStr TYPE@CODE | |||
* @return UserDetails | |||
* @throws UsernameNotFoundException | |||
*/ | |||
@Override | |||
@SneakyThrows | |||
public UserDetails loadUserBySocial(String inStr) { | |||
return getUserDetails(remoteUserService.social(inStr, SecurityConstants.FROM_IN)); | |||
} | |||
/** | |||
* 构建userdetails | |||
* @param result 用户信息 | |||
* @return | |||
*/ | |||
private UserDetails getUserDetails(R<UserInfo> result) { | |||
if (result == null || result.getData() == null) { | |||
throw new UsernameNotFoundException("用户不存在"); | |||
} | |||
UserInfo info = result.getData(); | |||
Set<String> dbAuthsSet = new HashSet<>(); | |||
if (ArrayUtil.isNotEmpty(info.getRoles())) { | |||
// 获取角色 | |||
Arrays.stream(info.getRoles()).forEach(roleId -> dbAuthsSet.add(SecurityConstants.ROLE + roleId)); | |||
// 获取资源 | |||
dbAuthsSet.addAll(Arrays.asList(info.getPermissions())); | |||
} | |||
Collection<? extends GrantedAuthority> authorities = AuthorityUtils | |||
.createAuthorityList(dbAuthsSet.toArray(new String[0])); | |||
SysUser user = info.getSysUser(); | |||
boolean enabled = StrUtil.equals(user.getLockFlag(), CommonConstants.STATUS_NORMAL); | |||
// 构造security用户 | |||
return new PigxUser(user.getUserId(), user.getDeptId(), user.getPhone(), user.getAvatar(), user.getTenantId(), | |||
user.getUsername(), SecurityConstants.BCRYPT + user.getPassword(), enabled, true, true, | |||
!CommonConstants.STATUS_LOCK.equals(user.getLockFlag()), authorities); | |||
} | |||
} |
@@ -0,0 +1,17 @@ | |||
server: | |||
port: 3000 | |||
spring: | |||
application: | |||
name: @artifactId@ | |||
cloud: | |||
nacos: | |||
discovery: | |||
server-addr: ${NACOS_HOST:pigx-register}:${NACOS_PORT:8848} | |||
config: | |||
server-addr: ${spring.cloud.nacos.discovery.server-addr} | |||
file-extension: yml | |||
shared-configs: | |||
- application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension} | |||
profiles: | |||
active: @profiles.active@ |
@@ -0,0 +1,87 @@ | |||
<?xml version="1.0" encoding="UTF-8"?> | |||
<!-- | |||
~ 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) | |||
--> | |||
<!-- | |||
小技巧: 在根pom里面设置统一存放路径,统一管理方便维护 | |||
<properties> | |||
<log-path>/Users/lengleng</log-path> | |||
</properties> | |||
1. 其他模块加日志输出,直接copy本文件放在resources 目录即可 | |||
2. 注意修改 <property name="${log-path}/log.path" value=""/> 的value模块 | |||
--> | |||
<configuration debug="false" scan="false"> | |||
<property name="log.path" value="logs/${project.artifactId}"/> | |||
<!-- 彩色日志格式 --> | |||
<property name="CONSOLE_LOG_PATTERN" | |||
value="${CONSOLE_LOG_PATTERN:-%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/> | |||
<!-- 彩色日志依赖的渲染类 --> | |||
<conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter"/> | |||
<conversionRule conversionWord="wex" | |||
converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter"/> | |||
<conversionRule conversionWord="wEx" | |||
converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter"/> | |||
<!-- Console log output --> | |||
<appender name="console" class="ch.qos.logback.core.ConsoleAppender"> | |||
<encoder> | |||
<pattern>${CONSOLE_LOG_PATTERN}</pattern> | |||
</encoder> | |||
</appender> | |||
<!-- Log file debug output --> | |||
<appender name="debug" class="ch.qos.logback.core.rolling.RollingFileAppender"> | |||
<file>${log.path}/debug.log</file> | |||
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"> | |||
<fileNamePattern>${log.path}/%d{yyyy-MM, aux}/debug.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern> | |||
<maxFileSize>50MB</maxFileSize> | |||
<maxHistory>30</maxHistory> | |||
</rollingPolicy> | |||
<encoder> | |||
<pattern>%date [%thread] %-5level [%logger{50}] %file:%line - %msg%n</pattern> | |||
</encoder> | |||
</appender> | |||
<!-- Log file error output --> | |||
<appender name="error" class="ch.qos.logback.core.rolling.RollingFileAppender"> | |||
<file>${log.path}/error.log</file> | |||
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"> | |||
<fileNamePattern>${log.path}/%d{yyyy-MM}/error.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern> | |||
<maxFileSize>50MB</maxFileSize> | |||
<maxHistory>30</maxHistory> | |||
</rollingPolicy> | |||
<encoder> | |||
<pattern>%date [%thread] %-5level [%logger{50}] %file:%line - %msg%n</pattern> | |||
</encoder> | |||
<filter class="ch.qos.logback.classic.filter.ThresholdFilter"> | |||
<level>ERROR</level> | |||
</filter> | |||
</appender> | |||
<logger name="org.activiti.engine.impl.db" level="DEBUG"> | |||
<appender-ref ref="debug"/> | |||
</logger> | |||
<!--nacos 心跳 INFO 屏蔽--> | |||
<logger name="com.alibaba.nacos" level="OFF"> | |||
<appender-ref ref="error"/> | |||
</logger> | |||
<!-- Level: FATAL 0 ERROR 3 WARN 4 INFO 6 DEBUG 7 --> | |||
<root level="INFO"> | |||
<appender-ref ref="console"/> | |||
<appender-ref ref="debug"/> | |||
</root> | |||
</configuration> |
@@ -0,0 +1,67 @@ | |||
/* | |||
* 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; | |||
} |
@@ -0,0 +1,51 @@ | |||
<!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> | |||
<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"> | |||
<a target="_blank" href="https://pig4cloud.com">${user.username}</a> | |||
</p> | |||
</div> | |||
</div> | |||
</nav> | |||
<div style="padding-top: 80px;width: 300px; color: #555; margin:0px auto;"> | |||
<form id='confirmationForm' name='confirmationForm' action="/oauth/authorize" method='post'> | |||
<input name='user_oauth_approval' value='true' type='hidden'/> | |||
<p> | |||
<a href="${app.website!''}" target="_blank">${app.appName!'未定义应用名称'}</a> 将获得以下权限:</p> | |||
<ul class="list-group"> | |||
<li class="list-group-item"> <span> | |||
<#list scopeList as scope> | |||
<input type="hidden" name="${scope}" value="true"/> | |||
<input type="checkbox" disabled checked="checked"/><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> |
@@ -0,0 +1,34 @@ | |||
<!DOCTYPE html> | |||
<html lang="en"> | |||
<head> | |||
<meta charset="utf-8"> | |||
<meta http-equiv="X-UA-Compatible" content="IE=edge"> | |||
<meta name="viewport" content="width=device-width, initial-scale=1"> | |||
<!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags --> | |||
<meta name="description" content=""> | |||
<meta name="author" content=""> | |||
<title>PigX微服务统一认证</title> | |||
<link href="/css/bootstrap.min.css" rel="stylesheet"> | |||
<link href="/css/signin.css" rel="stylesheet"> | |||
</head> | |||
<body class="sign_body"> | |||
<div class="container form-margin-top"> | |||
<form class="form-signin" action="/token/form" method="post"> | |||
<h2 class="form-signin-heading" align="center">统一认证系统</h2> | |||
<input type="text" name="username" class="form-control form-margin-top" placeholder="账号" required autofocus> | |||
<input type="password" name="password" class="form-control" placeholder="密码" required> | |||
<button class="btn btn-lg btn-primary btn-block" type="submit">sign in</button> | |||
<#if error??> | |||
<span style="color: red; ">${error}</span> | |||
</#if> | |||
</form> | |||
</div> | |||
<footer> | |||
<p>support by: pig4cloud</p> | |||
<p>email: <a href="mailto:pig4cloud@qq.com">pig4cloud@qq.com</a>.</p> | |||
</footer> | |||
</body> | |||
</html> |