底部附源码地址
功能介绍
- 集成验证码登录认证
- 加入密码输入错误次数超过限制次数,锁定账号
- 加入用户登录日志
- 基于Oauth2协议的密码登录和token刷新
项目结构
一个公共模块、一个sso模块、可以新加项目增加自己的业务模块
服务搭建
父级引入pom
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
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>
<groupId>com.yx</groupId>
<artifactId>auth-platform</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<modules>
<module>auth-common</module>
<module>auth-sso</module>
</modules>
<name>auth-platform</name>
<description>登录认证系统</description>
<properties>
<fast.version>1.0.0</fast.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<maven-jar-plugin.version>3.1.1</maven-jar-plugin.version>
<druid.version>1.2.16</druid.version>
<bitwalker.version>1.21</bitwalker.version>
<swagger.version>3.0.0</swagger.version>
<kaptcha.version>2.3.3</kaptcha.version>
<fastjson.version>2.0.39</fastjson.version>
<oshi.version>6.4.4</oshi.version>
<commons.io.version>2.13.0</commons.io.version>
<commons.collections.version>3.2.2</commons.collections.version>
<poi.version>4.1.2</poi.version>
<velocity.version>2.3</velocity.version>
<jwt.version>0.9.1</jwt.version>
<mybatis-plus.version>3.5.2</mybatis-plus.version>
<lombok.version>1.18.10</lombok.version>
<hutool.version>5.1.0</hutool.version>
<redisson.version>3.11.0</redisson.version>
<spring-boot-version>2.5.15</spring-boot-version>
<oauth2.version>2.2.6.RELEASE</oauth2.version>
</properties>
<!-- 依赖声明 -->
<dependencyManagement>
<dependencies>
<!-- SpringBoot的依赖配置-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot-version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- 阿里数据库连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>${oauth2.version}</version>
</dependency>
<!--工具包-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
<!-- io常用工具类 -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>${commons.io.version}</version>
</dependency>
<!-- collections工具类 -->
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>${commons.collections.version}</version>
</dependency>
<!-- 阿里JSON解析器 -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>${fastjson.version}</version>
</dependency>
<!-- Token生成与解析-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>${jwt.version}</version>
</dependency>
<!-- 解析客户端操作系统、浏览器等 -->
<dependency>
<groupId>eu.bitwalker</groupId>
<artifactId>UserAgentUtils</artifactId>
<version>${bitwalker.version}</version>
</dependency>
<!-- 验证码 -->
<dependency>
<groupId>pro.fessional</groupId>
<artifactId>kaptcha</artifactId>
<version>${kaptcha.version}</version>
</dependency>
<!-- 公共模块-->
<dependency>
<groupId>com.yx</groupId>
<artifactId>auth-common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!-- 认证模块-->
<dependency>
<groupId>com.yx</groupId>
<artifactId>auth-sso</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<encoding>${project.build.sourceEncoding}</encoding>
</configuration>
</plugin>
</plugins>
</build>
<!--依赖下载仓库地址-->
<repositories>
<repository>
<id>public</id>
<name>ali nexus</name>
<url>https://maven.aliyun.com/repository/public</url>
<releases>
<enabled>true</enabled>
</releases>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>public</id>
<name>ali nexus</name>
<url>https://maven.aliyun.com/repository/public</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
</project>
公共模块pom依赖
<!-- Spring框架基本的核心工具 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--常用工具类 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!--工具包-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<!-- 阿里JSON解析器 -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
</dependency>
<!-- JSON工具类 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10.1</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.1-android</version>
</dependency>
<!-- redis 缓存操作 -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>${redisson.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- Mysql驱动包 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
SSO模块pom依赖
<!-- SpringBoot Web容器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- SpringBoot 拦截器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- Auth2.0-->
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>eu.bitwalker</groupId>
<artifactId>UserAgentUtils</artifactId>
</dependency>
<!-- 阿里JSON解析器 -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
</dependency>
<!-- 验证码 -->
<dependency>
<groupId>pro.fessional</groupId>
<artifactId>kaptcha</artifactId>
<exclusions>
<exclusion>
<artifactId>servlet-api</artifactId>
<groupId>javax.servlet</groupId>
</exclusion>
</exclusions>
</dependency>
<!-- pool 对象池 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- 阿里数据库连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
</dependency>
<!-- 通用工具-->
<dependency>
<groupId>com.yx</groupId>
<artifactId>auth-common</artifactId>
</dependency>
核心配置类
JWT配置
package com.yx.config;
import com.yx.exception.MyWebResponseExceptionTranslator;
import com.yx.service.impl.MyUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.ClassPathResource;
import org.springframework.security.authentication.AuthenticationManager;
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.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory;
import javax.sql.DataSource;
import java.util.Arrays;
/**
* oauth2.0集成JWT
* @author cyx
*
*/
@Configuration
@EnableAuthorizationServer
public class JwtAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private TokenEnhancer jwtTokenEnhancer;
@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;
@Autowired
MyUserDetailsService myUserDetailsService;
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
// 让/oauth/token支持client_id以及client_secret作登录认证
// 请求/oauth/token的,如果配置支持allowFormAuthenticationForClients的,且url中有client_id和client_secret的会走ClientCredentialsTokenEndpointFilter
security.allowFormAuthenticationForClients();
security.checkTokenAccess("permitAll()");
security.tokenKeyAccess("permitAll()");
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.jdbc(dataSource);
}
@Override//
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(jwtTokenStore());
//设置RefreshTokens为不复用, true:复用是指每次刷新生成的refreshToken的jti不变,所以过期时间使用的还是第一次生成refreshToken的过期时间
//false:不复用是每次都生成新的jti,新的过期时间
endpoints.reuseRefreshTokens(false);
//指定认证管理器
endpoints.authenticationManager(authenticationManager);
TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
enhancerChain.setTokenEnhancers(Arrays.asList(jwtTokenEnhancer,jwtAccessTokenConverter));
endpoints.tokenEnhancer(enhancerChain);
endpoints.accessTokenConverter(jwtAccessTokenConverter);
// 转换异常信息
endpoints.exceptionTranslator(new MyWebResponseExceptionTranslator<>());
//增强token 包含用户信息
endpoints.userDetailsService(myUserDetailsService);
}
@Primary
@Bean
public DefaultTokenServices tokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(jwtTokenStore());
defaultTokenServices.setSupportRefreshToken(true);
TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
enhancerChain.setTokenEnhancers(Arrays.asList(jwtTokenEnhancer,jwtAccessTokenConverter));
defaultTokenServices.setTokenEnhancer(enhancerChain);
return defaultTokenServices;
}
/**
* JWT token 转换器
* @return
*/
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter jatc = new JwtAccessTokenConverter();
KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("oauth2.jks"), "LwABFa2JEJR3ezNG".toCharArray());
jatc.setKeyPair(keyStoreKeyFactory.getKeyPair("oauth2"));
return jatc;
}
/**
* JWT token Store配置
* @return
*/
@Bean
public JwtTokenStore jwtTokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
/**
* 自定义的Token增强器,把更多信息放入Token中
* @return
*/
@Bean
public TokenEnhancer jwtTokenEnhancer(){
return new JwtTokenEnhancer();
}
}
JWT增强
package com.yx.config;
import com.yx.constant.Constants;
import com.yx.model.UserInfo;
import com.yx.manager.AsyncManager;
import com.yx.manager.factory.AsyncFactory;
import com.yx.model.MyUser;
import com.yx.service.impl.SysPasswordService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
import java.util.HashMap;
import java.util.Map;
/**
* 扩展JWT附加信息
* @author cyx
*
*/
public class JwtTokenEnhancer implements TokenEnhancer {
@Autowired
private SysPasswordService sysPasswordService;
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
MyUser user = (MyUser)authentication.getUserAuthentication().getPrincipal();
UserInfo userInfo = user.getUserInfo();
Map<String,Object> info = new HashMap<>(16);
info.put("username" , userInfo.getUsername());
info.put("nickname", userInfo.getNickname());
info.put("id",userInfo.getId());
//设置附加信息
((DefaultOAuth2AccessToken)accessToken).setAdditionalInformation(info);
//记录登录日志
AsyncManager.me().execute(AsyncFactory.recordLoginInfo(userInfo.getUsername(), Constants.LOGIN_SUCCESS, "登录成功"));
//登录成功,清除错误日志缓存
sysPasswordService.clearLoginRecordCache(userInfo.getUsername());
return accessToken;
}
}
密码错误次数限制
package com.yx.config;
import com.yx.execption.ErrorCodeEnum;
import com.yx.service.impl.SysPasswordService;
import com.yx.utils.SpringUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 密码错误次数限制
*/
@Slf4j
@Component
public class LoginRetryCountFilter extends OncePerRequestFilter {
@Autowired
private CustomAuthenticationFailureHandler customAuthenticationFailureHandler;
private final static String OAUTH_TOKEN_URL = "/oauth/token";
@Override
public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
if (request.getServletPath().equals(OAUTH_TOKEN_URL) &&
StringUtils.equalsAnyIgnoreCase(request.getMethod(), "post")) {
String username = request.getParameter("username");
SysPasswordService passwordService = SpringUtils.getBean("sysPasswordService");
if (passwordService.isLock(username)) {
customAuthenticationFailureHandler.onAuthenticationFailure(request, response, new BadCredentialsException(ErrorCodeEnum.USER_PASSWORD_MAX_RETRY_COUNT.getMessage()));
return;
}
}
// 无异常即校验成功,放行。
filterChain.doFilter(request, response);
}
}
验证码校验
package com.yx.config;
import com.yx.constant.CacheConstants;
import com.yx.constant.Constants;
import com.yx.execption.ErrorCodeEnum;
import com.yx.manager.AsyncManager;
import com.yx.manager.factory.AsyncFactory;
import org.apache.commons.lang3.StringUtils;
import org.redisson.api.RBucket;
import org.redisson.api.RedissonClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class ValidateCodeFilter extends OncePerRequestFilter {
private static final Logger logger = LoggerFactory.getLogger("sys-user");
@Autowired
private RedissonClient redissonClient;
private final static String OAUTH_TOKEN_URL = "/oauth/token";
private final static String REFRESH_TOKEN = "refresh_token";
@Value("${sys.account.captchaEnabled:false}")
private boolean enabled;
@Autowired
private CustomAuthenticationFailureHandler customAuthenticationFailureHandler;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// authentication/form是认证时的请求接口,验证码校验只需要匹配这个接口即可
if (request.getServletPath().equals(OAUTH_TOKEN_URL) &&
StringUtils.equalsAnyIgnoreCase(request.getMethod(), HttpMethod.POST.toString())) {
String grantType = request.getParameter("grant_type");
//如果是刷新token,则不需要校验验证码
if (!REFRESH_TOKEN.equals(grantType)) {
//获取验证码开关
if (enabled) {
// 从redis中获取验证码
String id = request.getParameter("id");
// 从客户端接收到的验证码
String captchaParam = request.getParameter("code");
if (StringUtils.isEmpty(id)) {
customAuthenticationFailureHandler.onAuthenticationFailure(request, response, new BadCredentialsException("缺少必要的参数"));
return;
}
if (StringUtils.isEmpty(captchaParam)) {
customAuthenticationFailureHandler.onAuthenticationFailure(request, response, new BadCredentialsException(ErrorCodeEnum.CAPTCHA_NULL_EXCEPTION.getMessage()));
return;
}
//获取验证码
RBucket<String> bucket = redissonClient.getBucket(CacheConstants.CAPTCHA_CODE_KEY + id);
String code = bucket.get();
logger.debug("登录获取验证码:key:{},code:{}", id, code);
if (!StringUtils.equalsAnyIgnoreCase(captchaParam, code)) {
customAuthenticationFailureHandler.onAuthenticationFailure(request, response, new BadCredentialsException(ErrorCodeEnum.CAPTCHA_ERROR_EXCEPTION.getMessage()));
AsyncManager.me().execute(AsyncFactory.recordLoginInfo(request.getParameter("username"), Constants.LOGIN_FAIL, "验证码错误"));
return;
}
}
}
}
// 无异常即校验成功,放行。
filterChain.doFilter(request, response);
}
}
请求案例
通过密码模式获取令牌
http://localhost:9091/oauth/token?password=admin123&grant_type=password&client_id=pda_client&client_secret=user123&captcha=123&id=fb400c0b672c4be39b8c422edb2f801&username=admin2
刷新Token
http://localhost:9091/oauth/token?grant_type=refresh_token&client_id=pda_client&client_secret=user123&refresh_token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbjIiLCJzY29wZSI6WyJhbGwiXSwiYXRpIjoiNDUxN2NlNTUtNWNjNS00ODI5LWJkM2QtOWM4NjJkMWE0OTcyIiwibmlja25hbWUiOiJhZG1pbiIsImlkIjoyLCJleHAiOjE2OTc3MDA3MTEsImp0aSI6IjQyYjU2ZDhlLTViMGMtNDA4Mi1hODFmLWMzMDBhOWI5YjkzZCIsImNsaWVudF9pZCI6InBkYV9jbGllbnQiLCJ1c2VybmFtZSI6ImFkbWluMiJ9.D1xjWEuhtoCKpMBJKjauBBvYRBIBmOx3ItckyDns5qIOf_sUTNDtSgGlQ-4kCfIBBDSD23g4sTw2mvkMB5Ur3w
不足之处多多指教
还有什么常见的功能。欢迎留下宝贵意见!