前言

  • 一、maven依赖
  • 二、application.yml 配置
  • 三、security 的配置对应文件
  • 四、对校验提供provider
  • 五、权限验证具体实现类
  • 六、token、用户具体实现类
  • 七、需要handler(成功、失败、禁止)类
  • 八、自定义异常类
  • 九、支撑vo
  • 十、工具类

现在基本每个系统都有单点登录,基本稍微大一点公司都有自己搭建一套单点登录系统,其他所有项目直接接入。但有时候独立项目,需要自己实现一套登录系统,如果网上很多时候的代码很难拿来即用,每次自己全新构造一个又过于麻烦。我这里尝试弄一套登录系统,我们后续再做改动可以基于该模板进行改动。

实践

  • 一、maven依赖
  • 二、application.yml 配置
  • 三、security 的配置对应文件
  • 四、对校验提供provider
  • 五、权限验证具体实现类
  • 六、token、用户具体实现类
  • 七、需要handler(成功、失败、禁止)类
  • 八、自定义异常类
  • 九、支撑vo
  • 十、工具类

一、maven依赖

     <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><!-- 加载jdbc连接数据库 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId></dependency><!-- 加载mybatis jar包 --><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.3.2</version></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.10</version><scope>compile</scope></dependency><!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-security --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId><version>2.3.6.RELEASE</version></dependency><!-- https://mvnrepository.com/artifact/org.springframework.security.oauth.boot/spring-security-oauth2-autoconfigure --><dependency><groupId>org.springframework.security.oauth.boot</groupId><artifactId>spring-security-oauth2-autoconfigure</artifactId><version>2.3.5.RELEASE</version></dependency><!-- https://mvnrepository.com/artifact/cn.hutool/hutool-all --><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.5.7</version></dependency><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.73</version></dependency><!-- https://mvnrepository.com/artifact/com.spring4all/swagger-spring-boot-starter --><dependency><groupId>com.spring4all</groupId><artifactId>swagger-spring-boot-starter</artifactId><version>1.9.0.RELEASE</version></dependency><!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt --><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.1</version></dependency><dependency><groupId>nl.bitwalker</groupId><artifactId>UserAgentUtils</artifactId><version>1.2.4</version></dependency><dependency><groupId>eu.bitwalker</groupId><artifactId>UserAgentUtils</artifactId><version>1.20</version></dependency><dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId><version>2.9.0</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId><version>2.3.4.RELEASE</version></dependency><dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId><version>3.3.0</version></dependency><dependency><groupId>org.hibernate.validator</groupId><artifactId>hibernate-validator</artifactId><version>6.1.5.Final</version></dependency><dependency><groupId>com.github.xiaoymin</groupId><artifactId>knife4j-spring-boot-starter</artifactId><version>2.0.5</version></dependency>

二、application.yml 配置

server:port: 8082
spring:datasource:driverClassName: com.mysql.cj.jdbc.Drivertype: com.zaxxer.hikari.HikariDataSourceurl: jdbc:mysql://localhost:3306/test_mybatis?serverTimezone=Asia/Shanghaiusername: rootpassword: 123456hikari:read-only: false#客户端等待连接池连接的最大毫秒数connection-timeout: 60000#允许连接在连接池中空闲的最长时间(以毫秒为单位)idle-timeout: 60000#连接将被测试活动的最大时间量validation-timeout: 3000#池中连接关闭后的最长生命周期max-lifetime: 60000#最大池大小maximum-pool-size: 60#连接池中维护的最小空闲连接数minimum-idle: 10#从池返回的连接的默认自动提交行为。默认值为trueauto-commit: true#如果您的驱动程序支持JDBC4,我们强烈建议您不要设置此属性connection-test-query: SELECT 1#自定义连接池名称pool-name: myHikarCpsso:token:header: Authorizationsecret: abcdefghijklmnopqrstuvwxyzexpire-time: 30auth:perms-prefix: authretry-limit: falseretry-count: 5retry-time: 30retry-msg: "您的账号超出最大重试次数限制,请稍后再试"ignore-access-url:- /login- /register- /captchaImage- /captchaJsonforbid-access-url:- rootlogin-access-url:- visitfeign-super: false

三、security 的配置对应文件

spring security 关于校验规则的配置

/*** spring security配置**/
@Slf4j
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) //开启方法级别安全验证
public class SecurityConfig extends WebSecurityConfigurerAdapter {/*** 自定义用户认证逻辑*/@Resourceprivate UserDetailsService userDetailsService;@Resourceprivate LogoutSuccessHandlerImpl logoutSuccessHandler;@Resourceprivate SsoAuthConfig ssoAuthConfig;/*** 解决 无法直接注入 AuthenticationManager** @return AuthenticationManager*/@Bean@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}/*** anyRequest          |   匹配所有请求路径* access              |   SpringEl表达式结果为true时可以访问* anonymous           |   匿名可以访问* denyAll             |   用户不能访问* fullyAuthenticated  |   用户完全认证可以访问(非remember-me下自动登录)* hasAnyAuthority     |   如果有参数,参数表示权限,则其中任何一个权限可以访问* hasAnyRole          |   如果有参数,参数表示角色,则其中任何一个角色可以访问* hasAuthority        |   如果有参数,参数表示权限,则其权限可以访问* hasIpAddress        |   如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问* hasRole             |   如果有参数,参数表示角色,则其角色可以访问* permitAll           |   用户可以任意访问* rememberMe          |   允许通过remember-me登录的用户访问* authenticated       |   用户登录后可访问*/@Overrideprotected void configure(HttpSecurity httpSecurity) throws Exception {httpSecurity// CSRF禁用.csrf().disable()// 基于token,所以不需要session.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()// 允许跨域.cors().and().headers().addHeaderWriter(new StaticHeadersWriter(Collections.singletonList(new Header("Access-Control-Expose-Headers", "Authorization"))));// 匿名访问路径if (ArrayUtil.isNotEmpty(ssoAuthConfig.getIgnoreAccessUrl())) {httpSecurity.authorizeRequests().antMatchers(ssoAuthConfig.getIgnoreAccessUrl()).permitAll();}httpSecurity// 过滤请求.authorizeRequests()// 自定义权限验证规则.anyRequest().access("@ss.hasPermission()").and()//禁用自带的页面表单登陆,自定义登陆接口.formLogin().disable().logout().logoutUrl("/logout")//退出登陆成功.logoutSuccessHandler(logoutSuccessHandler).permitAll().and().headers().frameOptions().sameOrigin();httpSecurity.exceptionHandling()// 已认证用户访问无权限资源时的异常.accessDeniedHandler(new AccessDeniedHandlerImpl())// 匿名用户访问无权限资源时的异常.authenticationEntryPoint(new AuthenticationEntryPointImpl());}@Overridepublic void configure(WebSecurity web) {web.ignoring().antMatchers(HttpMethod.OPTIONS, "/**").antMatchers(HttpMethod.GET, "/*.html", "/**/*.html", "/**/*.css", "/**/*.js").antMatchers("/swagger-resources").antMatchers("/swagger-resources/configuration/ui").antMatchers("/swagger-resources/configuration/security").antMatchers("/v2/api-docs").antMatchers("/v2/api-docs-ext");}/*** 强散列哈希加密实现*/@Beanpublic BCryptPasswordEncoder bCryptPasswordEncoder() {return new BCryptPasswordEncoder();}@Beanpublic MobileCodeAuthenticationProvider mobileCodeAuthenticationProvider() {return new MobileCodeAuthenticationProvider();}@Beanpublic UsernameAuthenticationProvider usernameAuthenticationProvider() {return new UsernameAuthenticationProvider();}@Overrideprotected void configure(AuthenticationManagerBuilder auth) {auth.authenticationProvider(usernameAuthenticationProvider()).authenticationProvider(mobileCodeAuthenticationProvider());}}

令牌配置1

/*** 令牌配置**/
@Data
@Configuration
@ConfigurationProperties(prefix = SsoAuthConfig.PREFIX)
public class SsoAuthConfig {public static final String PREFIX = "sso.auth";/*** 前缀*/private String permsPrefix;/*** 是否开启重试限制,默认开启*/private boolean retryLimit = true;/*** 重试最大次数,默认5次*/private int retryCount = 5;/*** 重试间隔时间,单位秒*/private int retryTime = 30;/*** 重试限制提示信息*/private String retryMsg = "您的账号超出最大重试次数限制,请稍后再试";/*** 匿名访问路径*/private String[] ignoreAccessUrl = {"/login", "/register", "/captchaImage", "/captchaJson"};/*** 禁止访问路径*/private List<AntPathRequestMatcher> forbidAccessUrl = Collections.emptyList();/*** 登陆后可访问路径*/private List<AntPathRequestMatcher> loginAccessUrl = Collections.emptyList();/*** feign调用放开超级权限*/private boolean feignSuper = false;}

令牌配置2

/*** 令牌配置*/
@Data
@Configuration
@ConfigurationProperties(prefix = SsoTokenConfig.PREFIX)
public class SsoTokenConfig {public static final String PREFIX = "sso.token";/*** 令牌自定义标识*/private String header = "Authorization";/*** 令牌秘钥*/private String secret = "test@secret";/*** 令牌有效期(默认30分钟)*/private Integer expireTime = 30;
}

附上swgger和redis的配置

swgger配置

@Configuration
@EnableSwagger2
@EnableKnife4j
public class Swagger2Config {@Beanpublic Docket createRestApi() {return  new Docket(DocumentationType.SWAGGER_2).useDefaultResponseMessages(false).apiInfo(apiInfo()).select().apis(RequestHandlerSelectors.basePackage("com.yin.demo")).paths(PathSelectors.any()).build();}private ApiInfo apiInfo() {return new ApiInfoBuilder().title("swagger-bootstrap-ui RESTful APIs").description("swagger-bootstrap-ui").termsOfServiceUrl("http://localhost:8082/").version("1.0").build();}
}

redis 的properties 文件

#客户端超时时间单位是毫秒 默认是2000
so.redis.timeout=30000connection.timeout=3000
#最大空闲数
redis.maxIdle=300
#连接池的最大数据库连接数。设为0表示无限制,如果是jedis 2.4以后用redis.maxTotal
#redis.maxActive=600
#控制一个pool可分配多少个jedis实例,用来替换上面的redis.maxActive,如果是jedis 2.4以后用该属性
redis.maxTotal=2000
#最大建立连接等待时间。如果超过此时间将接到异常。设为-1表示无限制。
redis.maxWaitMillis=1000
redis.max-attempts=3
redis.password=redis.nodes=120.76.142.xx:7001,120.76.142.xx:7002,120.76.142.xx:7003,120.76.142.xx:7004,120.76.142.xx:7005,120.76.142.xx:7006

redis config配置

/*** @author yinfeiyue*/
@Configuration
@PropertySource("classpath:conf/redis.properties")
public class RedisConfig {@Value("${redis.maxIdle}")private Integer maxIdle;/*** 连接超时时间*/@Value("${connection.timeout}")private int connectionTimeout;/*** 读取数据超时时间*/@Value("${so.redis.timeout}")private int soTimeout;@Value("${redis.maxTotal}")private Integer maxTotal;@Value("${redis.maxWaitMillis}")private Integer maxWaitMillis;@Value("${redis.max-attempts}")private Integer maxAttempts;@Value("${redis.password}")private String password;@Value("${redis.nodes}")private String clusterNodes;@Beanpublic JedisCluster getJedisCluster(){String[] cNodes = clusterNodes.split(",");HashSet<HostAndPort> nodes = new HashSet<>();//分割集群节点for (String node : cNodes) {String[] hp = node.split(":");nodes.add(new HostAndPort(hp[0], Integer.parseInt(hp[1])));}JedisPoolConfig jedisPoolConfig=new JedisPoolConfig();jedisPoolConfig.setMaxIdle(maxIdle);jedisPoolConfig.setMaxWaitMillis(maxWaitMillis);jedisPoolConfig.setMaxTotal(maxTotal);//创建集群对象//  JedisCluster jedisCluster =new JedisCluster(nodes, connectionTimeout, soTimeout, maxAttempts, password, jedisPoolConfig);JedisCluster jedisCluster =new JedisCluster(nodes, connectionTimeout, soTimeout, maxAttempts,  jedisPoolConfig);return jedisCluster;}/*** 默认过期时间*/@Value("${cache.default.expire-time:1800}")private int defaultExpireTime;static {ParserConfig.getGlobalInstance().setAutoTypeSupport(true);}@Beanpublic RedisConnectionFactory connectionFactory() {JedisPoolConfig poolConfig = new JedisPoolConfig();//poolConfig.setMaxTotal(maxActive);poolConfig.setMaxIdle(maxIdle);poolConfig.setMaxWaitMillis(maxWaitMillis);//poolConfig.setMinIdle(minIdle);poolConfig.setTestOnBorrow(true);poolConfig.setTestOnReturn(false);poolConfig.setTestWhileIdle(true);JedisClientConfiguration clientConfig = JedisClientConfiguration.builder().usePooling().poolConfig(poolConfig).and().readTimeout(Duration.ofMillis(soTimeout)).build();// 单点redis
//        RedisStandaloneConfiguration redisConfig = new RedisStandaloneConfiguration();// 哨兵redis// RedisSentinelConfiguration redisConfig = new RedisSentinelConfiguration();// 集群redisRedisClusterConfiguration redisConfig = new RedisClusterConfiguration();HashSet<RedisNode> nodes = new HashSet<>();//分割集群节点String[] cNodes = clusterNodes.split(",");for (String node : cNodes) {String[] hp = node.split(":");nodes.add(new RedisNode(hp[0], Integer.parseInt(hp[1])));}redisConfig.setClusterNodes(nodes);// redisConfig.setPassword(RedisPassword.of(redisAuth));return new JedisConnectionFactory(redisConfig,clientConfig);}/*** 缓存管理器** @param redisConnectionFactory 连接工厂* @return org.springframework.cache.CacheManager*/@Beanpublic CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {RedisCacheConfiguration cacheConfig = RedisCacheConfiguration.defaultCacheConfig();// 设置缓存管理器管理的缓存的默认过期时间cacheConfig.entryTtl(Duration.ofSeconds(defaultExpireTime));// 不缓存空值cacheConfig.disableCachingNullValues();return RedisCacheManager.builder(redisConnectionFactory).cacheDefaults(cacheConfig).build();}/*** 重写Redis序列化方式,使用Json方式:* 当我们的数据存储到Redis的时候,我们的键(key)和值(value)都是通过Spring提供的Serializer序列化到数据库的。* RedisTemplate默认使用的是JdkSerializationRedisSerializer,* StringRedisTemplate默认使用的是StringRedisSerializer。* Spring Data JPA为我们提供了下面的Serializer:* GenericToStringSerializer、Jackson2JsonRedisSerializer、StringRedisSerializer、* JacksonJsonRedisSerializer、JdkSerializationRedisSerializer、OxmSerializer。* 在此我们将自己配置RedisTemplate并定义Serializer。** @param redisConnectionFactory 连接工厂* @return RedisTemplate*/@Bean(name = "redisTemplate")@Primary@ConditionalOnMissingBean(name = "redisTemplate")public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();redisTemplate.setConnectionFactory(redisConnectionFactory);//设置序列化Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);ObjectMapper om = new ObjectMapper();om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);jackson2JsonRedisSerializer.setObjectMapper(om);RedisSerializer stringSerializer = new StringRedisSerializer();//key序列化redisTemplate.setKeySerializer(stringSerializer);//value序列化redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);//Hash key序列化redisTemplate.setHashKeySerializer(stringSerializer);//Hash value序列化redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);redisTemplate.afterPropertiesSet();return redisTemplate;}}

四、对校验提供provider

/*** 手机验证码登录Provider**/
@Slf4j
public class MobileCodeAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {@Autowiredprivate UserDetailsService userDetailsService;@Overrideprotected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) {// 暂不处理}@Overridepublic Authentication authenticate(Authentication authentication) {MobileCodeAuthenticationToken tokenReq = (MobileCodeAuthenticationToken) authentication;try {//根据手机号码,查找登录人信息UserDetails userDetails = userDetailsService.loadUserByUsername(tokenReq.getMobile());return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());} catch (BadCredentialsException e1) {log.error(e1.getMessage());throw e1;} catch (Exception e) {log.error(e.getMessage());throw new BadCredentialsException("手机验证码登录异常:" + e.getMessage());}}@Overrideprotected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) {return null;}@Overridepublic boolean supports(Class<?> authentication) {return (MobileCodeAuthenticationToken.class.isAssignableFrom(authentication));}}
/*** 手机验证码登录**/
public class UsernameAuthenticationProvider implements AuthenticationProvider {protected Log logger = LogFactory.getLog(getClass());@Autowiredprivate UserDetailsService userDetailsService;@Autowiredprivate PasswordEncoder passwordEncoder;@Overridepublic Authentication authenticate(Authentication authentication) {UsernameAuthenticationToken tokenReq = (UsernameAuthenticationToken) authentication;// 根据手机号码,查找登录人信息....UserDetails userDetails = userDetailsService.loadUserByUsername(tokenReq.getLoginAcct());// 密码加密后比较是否匹配if(!passwordEncoder.matches(tokenReq.getPassword(), userDetails.getPassword())) {throw new BadCredentialsException("账号或密码错误");}return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());}@Overridepublic boolean supports(Class<?> authentication) {return (UsernameAuthenticationToken.class.isAssignableFrom(authentication));}}

五、权限验证具体实现类

@Slf4j
@Service("ss")
public class PermissionService {/*** 所有权限标识*/private static final String ALL_PERMISSION = "*:*:*";/*** 权限分隔符*/private static final String DELIMITER = ",";@Resourceprivate TokenService tokenService;@Resourceprivate SsoAuthConfig ssoAuthConfig;@Resourceprivate SsoTokenConfig ssoTokenConfig;/*** 验证是否有访问权限** @return boolean*/public boolean hasPermission() {HttpServletRequest request = WebUtils.getRequest();if (request == null) {return false;}if (StrUtil.isBlank(request.getHeader(ssoTokenConfig.getHeader()))) {return false;}// 禁止任何人访问路径if (ArrayUtil.isNotEmpty(ssoAuthConfig.getForbidAccessUrl())) {for (AntPathRequestMatcher matcher : ssoAuthConfig.getForbidAccessUrl()) {if (matcher.matcher(request).isMatch()) {throw new ForbiddenException(BusinessStatus.FORBIDDEN.getValue());}}}// 获取访问路径URLString requestUri = request.getRequestURI();String method = request.getMethod();String permission = requestUri.replaceFirst("/", "").replace("/", ":");if (StrUtil.isNotEmpty(ssoAuthConfig.getPermsPrefix())) {permission = ssoAuthConfig.getPermsPrefix() + ":" + method + ":" + permission;}if (StrUtil.isEmpty(permission)) {return false;}// 用户是否已经登陆LoginUser loginUser = tokenService.getLoginUser(request);if (Objects.nonNull(loginUser)) {UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));SecurityContextHolder.getContext().setAuthentication(authenticationToken);tokenService.verifyToken(loginUser);// 登陆后可以访问请求for (AntPathRequestMatcher matcher : ssoAuthConfig.getLoginAccessUrl()) {if (matcher.matcher(request).isMatch()) {return true;}}}// 没有任何访问权限if (Objects.isNull(loginUser) || CollUtil.isEmpty(loginUser.getPermissions())) {return false;}return hasPermissions(loginUser.getPermissions(), permission);}/*** 验证用户是否具有以下任意一个权限** @param permissions 以 PERMISSION_NAMES_DELIMITER 为分隔符的权限列表* @return 用户是否具有以下任意一个权限*/public boolean hasAnyPermission(String permissions) {if (StrUtil.isEmpty(permissions)) {return false;}LoginUser loginUser = tokenService.getLoginUser(WebUtils.getRequest());if (Objects.isNull(loginUser) || CollUtil.isEmpty(loginUser.getPermissions())) {return false;}Set<String> authorities = loginUser.getPermissions();for (String permission : permissions.split(DELIMITER)) {if (permission != null && hasPermissions(authorities, permission)) {return true;}}return false;}/*** 判断是否包含权限** @param permissions 权限列表* @param permission  权限字符串* @return 用户是否具备某权限*/private boolean hasPermissions(Set<String> permissions, String permission) {return permissions.contains(ALL_PERMISSION) || permissions.contains(StrUtil.trim(permission));}
}

六、token、用户具体实现类

/*** token验证处理*/
@Slf4j
@Component
public class TokenService {@Resourceprivate SsoTokenConfig ssoTokenConfig;private static final long MILLIS_SECOND = 1000;private static final long MILLIS_MINUTE = 60 * MILLIS_SECOND;@Resourceprivate RedisTemplate<String, LoginUser> redisTemplate;/*** 获取用户身份信息** @return 登陆用户信息*/public LoginUser getLoginUser(HttpServletRequest request) {// 获取请求携带的令牌String token = getToken(request);return getLoginUserByToken(token);}/*** 根据token获取用户身份信息** @param token 身份标识* @return 登陆用户*/public LoginUser getLoginUserByToken(String token) {if (StrUtil.isNotEmpty(token)) {Claims claims = parseToken(token);if (!Objects.isNull(claims)) {// 解析对应的权限以及用户信息String uuid = (String) claims.get(UserConstants.LOGIN_USER_KEY);String userKey = getTokenKey(uuid);return redisTemplate.opsForValue().get(userKey);}}return null;}/*** 更新用户信息后,刷新缓存中用户信息* @param loginUser*/public void refreshLoginUser(LoginUser loginUser){// 根据uuid将loginUser缓存String userKey = getTokenKey(loginUser.getUuid());redisTemplate.opsForValue().set(userKey, loginUser, ssoTokenConfig.getExpireTime(), TimeUnit.MINUTES);}/*** 创建令牌** @param loginUser 用户信息* @return 令牌*/public String createToken(LoginUser loginUser) {String uuid = loginUser.getUuid();if(StringUtils.isBlank(loginUser.getUuid())){uuid = IdUtil.simpleUUID();loginUser.setUuid(uuid);}Map<String, Object> claims = new HashMap<>(8);claims.put(UserConstants.LOGIN_USER_KEY, uuid);String jwtToken = Jwts.builder().setClaims(claims).setSubject(loginUser.getUsername()).signWith(SignatureAlgorithm.HS512, ssoTokenConfig.getSecret()).compact();loginUser.setToken(jwtToken);setUserAgent(loginUser);refreshToken(loginUser);return jwtToken;}/*** 设置用户代理信息** @param loginUser 登录信息*/private void setUserAgent(LoginUser loginUser) {/*** User Agent中文名为用户代理,是Http协议中的一部分,属于头域的组成部分。* 它是一个特殊字符串头,是一种向访问网站提供你所使用的浏览器类型及版本、操作系统及版本、浏览器内核、等信息的标识。* 通过这个标识,用户所访问的网站可以显示不同的排版从而为用户提供更好的体验或者进行信息统计。*/if (Objects.nonNull(WebUtils.getRequest())) {loginUser.setIpAddr(WebUtils.getRequestIp());UserAgent userAgent = UserAgent.parseUserAgentString(WebUtils.getRequest().getHeader("User-Agent"));loginUser.setBrowser(userAgent.getBrowser().getName());loginUser.setOs(userAgent.getOperatingSystem().getName());}}/*** 验证令牌有效期,在指定的有效时间内,自动刷新缓存** @param loginUser 登陆用户*/public void verifyToken(LoginUser loginUser) {long expireTime = loginUser.getExpireTime();long currentTime = System.currentTimeMillis();if (expireTime - currentTime <= UserConstants.LOGIN_USER_TOKEN_REFRESH) {refreshToken(loginUser);}}/*** 刷新令牌有效期** @param loginUser 登录信息*/private void refreshToken(LoginUser loginUser) {loginUser.setLoginTime(System.currentTimeMillis());long millisecondsToExpire = ssoTokenConfig.getExpireTime() * MILLIS_MINUTE;loginUser.setExpireTime(loginUser.getLoginTime() + millisecondsToExpire);// 根据uuid将loginUser缓存String userKey = getTokenKey(loginUser.getUuid());redisTemplate.opsForValue().set(userKey, loginUser, ssoTokenConfig.getExpireTime(), TimeUnit.MINUTES);}/*** 从令牌中获取数据声明** @param token 令牌* @return 数据声明*/public Claims parseToken(String token) {try {return Jwts.parser().setSigningKey(ssoTokenConfig.getSecret()).parseClaimsJws(token).getBody();} catch (Exception e) {if (log.isDebugEnabled()) {log.debug("token:{}非法:{}", token, e.getMessage());}return null;}}/*** 从令牌中获取用户名** @param token 令牌* @return 用户名*/public String getUsernameFromToken(String token) {Claims claims = parseToken(token);return claims.getSubject();}/*** 获取请求token** @param request 请求对象* @return token*/private String getToken(HttpServletRequest request) {String token = request.getHeader(ssoTokenConfig.getHeader());if (StrUtil.isNotEmpty(token) && token.startsWith(UserConstants.LOGIN_USER_TOKEN_PREFIX)) {token = token.replace(UserConstants.LOGIN_USER_TOKEN_PREFIX, "").trim();}return token;}/*** 获取用户Redis Key** @param uuid 用户UUID* @return Redis key*/private String getTokenKey(String uuid) {return UserConstants.LOGIN_USER_TOKEN_KEY + uuid;}
}
@Slf4j
@Service
@Primary
public class UserDetailsServiceImpl implements UserDetailsService {@Overridepublic UserDetails loadUserByUsername(String username) {return createLoginUser(username);}/*** 创建UserDetails** @return UserDetails*/public UserDetails createLoginUser(String username) {LoginUser loginUser = new LoginUser();loginUser.setMobile("123456");loginUser.setGender(1);loginUser.setUuid(UUID.randomUUID().toString().replace("-", ""));loginUser.setLoginName("loginName");loginUser.setPassword(SecurityUtils.encryptPassword(username));loginUser.setPermissions(getMenuPermission());loginUser.setExpireTime(System.currentTimeMillis() + UserConstants.LOGIN_USER_TOKEN_REFRESH);return loginUser;}/*** 获取角色数据权限* @return 角色权限信息*/public Set<String> getRolePermission() {Set<String> roles = new HashSet<>(8);roles.add("admin");// 管理员拥有所有权限return roles;}/*** 获取菜单数据权限** @return 菜单权限信息*/public Set<String> getMenuPermission() {Set<String> roles = new HashSet<>(8);roles.add("*:*:*");// 管理员拥有所有权限return roles;}
}

七、需要handler(成功、失败、禁止)类

/*** 如果用户已经通过身份验证,试图访问受保护的(该用户没有权限的)资源*/
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {@Overridepublic void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {response.setStatus(HttpStatus.OK.value());response.setCharacterEncoding(StandardCharsets.UTF_8.name());response.setContentType(MediaType.APPLICATION_JSON_VALUE);response.getWriter().print(JSON.toJSONString(ResponseData.failure(BusinessStatus.FORBIDDEN.getKey(), BusinessStatus.FORBIDDEN.getValue())));response.getWriter().flush();}
}
/*** 未经过身份验证的用户试图访问受保护的资源**/
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {@Overridepublic void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException {response.setStatus(HttpStatus.OK.value());response.setCharacterEncoding(StandardCharsets.UTF_8.name());response.setContentType(MediaType.APPLICATION_JSON_VALUE);if (e instanceof ForbiddenException) {response.getWriter().print(JSON.toJSONString(ResponseData.failure(BusinessStatus.FORBIDDEN.getKey(), BusinessStatus.FORBIDDEN.getValue())));} else {response.getWriter().print(JSON.toJSONString(ResponseData.failure(BusinessStatus.UNAUTHORIZED.getKey(), BusinessStatus.UNAUTHORIZED.getValue())));}response.getWriter().flush();}
}
@Configuration
public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler {@Resourceprivate TokenService tokenService;@Resourceprivate RedisTemplate<String, LoginUser> redisTemplate;/*** 退出处理*/@Overridepublic void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {LoginUser loginUser;if (!Objects.isNull(authentication)) {loginUser = (LoginUser) authentication.getPrincipal();} else {loginUser = tokenService.getLoginUser(request);}if (!Objects.isNull(loginUser)) {//解析tokenClaims claims = tokenService.parseToken(loginUser.getToken());// 解析对应的权限以及用户信息String uuid = (String) claims.get(UserConstants.LOGIN_USER_KEY);String userKey = UserConstants.LOGIN_USER_TOKEN_KEY + uuid;//退出登录收回token令牌redisTemplate.delete(userKey);}response.setStatus(HttpStatus.OK.value());response.setCharacterEncoding(StandardCharsets.UTF_8.name());response.setContentType(MediaType.APPLICATION_JSON_VALUE);response.getWriter().print(JSON.toJSONString(ResponseData.success(BusinessStatus.SUCCESS.getKey(), "退出登陆成功")));}
}

八、自定义异常类

public class BusinessException extends RuntimeException {/*** 错误码*/private final int errorCode;public BusinessException() {this("业务异常");}public BusinessException(String errMsg) {this(errMsg, 0);}public BusinessException(String errMsg, Integer errorCode) {super(errMsg, null, false, false);this.errorCode = errorCode;}public Integer getErrorCode() {return errorCode;}}
/*** 禁止访问异常*/
public class ForbiddenException extends AuthenticationException {public ForbiddenException(String msg) {super(msg);}public ForbiddenException(String msg, Throwable t) {super(msg, t);}
}

九、支撑vo

public class MobileCodeAuthenticationToken extends AbstractAuthenticationToken {private String mobile;private String code;public MobileCodeAuthenticationToken(String mobile, String code) {super(null);this.mobile = mobile;this.code = code;}@Overridepublic Object getCredentials() {return null;}@Overridepublic Object getPrincipal() {return null;}public String getMobile() {return mobile;}public void setMobile(String mobile) {this.mobile = mobile;}public String getCode() {return code;}public void setCode(String code) {this.code = code;}
}
public class UsernameAuthenticationToken extends AbstractAuthenticationToken {private String loginAcct;private String password;public UsernameAuthenticationToken(String loginAcct, String password) {super(null);this.loginAcct = loginAcct;this.password = password;}@Overridepublic Object getCredentials() {return null;}@Overridepublic Object getPrincipal() {return null;}public String getLoginAcct() {return loginAcct;}public void setLoginAcct(String loginAcct) {this.loginAcct = loginAcct;}public String getPassword() {return password;}public void setPassword(String password) {this.password = password;}
}
/*** 登录用户身份权限**/
@Data
public class LoginUser implements UserDetails {/*** 用户唯一标识*/private String uuid;/*** 用户权限标识*/private String token;/*** 园区ID(默认为5,测试用)*/private Long parkId = 5L;/*** 用户ID*/private Long userId;/*** 用户姓名*/private String username;/*** 登录账号*/private String loginName;/*** 用户昵称*/@ApiModelProperty(value = "用户昵称" )private String nickname;/*** 密码*/private String password;/*** 手机号码*/private String mobile;/*** 登陆时间*/private Long loginTime;/*** 过期时间*/private Long expireTime;/*** 登录IP地址*/private String ipAddr;/*** 登录地点*/private String loginLocation;/*** 浏览器类型*/private String browser;/*** 操作系统*/private String os;/*** 用户头像*/private String portrait;/*** 性别*/private Integer gender;/*** 权限列表*/private Set<String> permissions;/*** 角色类型 变更为 用户分类字段, 此处数据来源于auth_user_park表中的user_classify字段*/private List<Integer> roleTypes;@JsonIgnore@JSONField(serialize = false)@Overridepublic String getPassword() {return password;}@Overridepublic String getUsername() {return username;}/*** 账户是否未过期,过期无法验证*/@JsonIgnore@JSONField(serialize = false)@Overridepublic boolean isAccountNonExpired() {return true;}/*** 指定用户是否解锁,锁定的用户无法进行身份验证** @return true*/@JsonIgnore@JSONField(serialize = false)@Overridepublic boolean isAccountNonLocked() {return true;}/*** 指示是否已过期的用户的凭据(密码),过期的凭据防止认证** @return true*/@JsonIgnore@JSONField(serialize = false)@Overridepublic boolean isCredentialsNonExpired() {return true;}/*** 是否可用 ,禁用的用户不能身份验证** @return true*/@JsonIgnore@JSONField(serialize = false)@Overridepublic boolean isEnabled() {return true;}@JsonIgnore@JSONField(serialize = false)@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {List<GrantedAuthority> grantedAuthorities = new ArrayList<>();if (CollUtil.isNotEmpty(permissions)) {for (String permission : permissions) {GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(permission);grantedAuthorities.add(grantedAuthority);}}return grantedAuthorities;}
}
/*** 登录参数**/
@Data
public class LoginPwdReq implements Serializable {/*** 账号*/@NotBlank(message = "账号不能为空")private String account;/*** 密码*/@NotBlank(message = "密码不能为空")private String password;
}
/*** 获取验证码参数**/
@Data
public class LoginPhoneReq implements Serializable {/*** 手机号码*/@NotBlank(message = "手机号码不能为空")@Pattern(regexp = "^((1[3-9][0-9])\\d{8})$", message = "手机号格式错误")private String account;
}
/*** 登录参数**/
@Data
public class LoginMessageCodeReq implements Serializable {/*** 账号*/@NotBlank(message = "手机号码不能为空")@Pattern(regexp = "^((1[3-9][0-9])\\d{8})$", message = "手机号格式错误")private String account;/*** 短信验证码*/@NotBlank(message = "短信验证码不能为空")private String code;
}

十、工具类

@Slf4j
public class RedisUtil {private static class SingletonRestTemplate {static RedisTemplate<String, Object> instance = (RedisTemplate<String, Object>) SpringUtils.getBean("redisTemplate");}private RedisUtil() {}public static RedisTemplate<String, Object> getInstance() {return SingletonRestTemplate.instance;}/*** 枷锁**/public static boolean deleteExclusionLock(String key) {if (RedisUtil.getInstance().delete(key)) {return true;}log.info("delete {}'s lock fail!", key);return false;}/*** 枷锁**/public static boolean exclusionLock(String key,Long timeOut) {log.info("try to get lock for {}", key);if (RedisUtil.getInstance().opsForValue().setIfAbsent(key,"1",timeOut,TimeUnit.MILLISECONDS)) {return true;}return false;}/*** 普通缓存获取* @param key 键* @return 值*/public static Object get(String key) {boolean hasKey = RedisUtil.getInstance().hasKey(key);return hasKey ? RedisUtil.getInstance().opsForValue().get(key) : "";}/*** 普通缓存放入* @param key 键* @param value 值* @return true成功 false失败*/public static boolean set(String key, Object value) {try {RedisUtil.getInstance().opsForValue().set(key, value);return true;} catch (Exception e) {log.error(e.getMessage(), e);return false;}}public static boolean mset(Map<String,Object> map) {try {RedisUtil.getInstance().opsForValue().multiSet(map);return true;} catch (Exception e) {log.error(e.getMessage(), e);return false;}}/*** 普通缓存放入并设置时间* @param key 键* @param value 值* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期* @param unit 时间单位* @return true成功 false 失败*/public static boolean set(String key, Object value, long time, TimeUnit unit) {try {if (time > 0) {RedisUtil.getInstance().opsForValue().set(key, value, time, unit);} else {set(key, value);}return true;} catch (Exception e) {log.error(e.getMessage(), e);return false;}}/*** 递增* @param key 键* @param delta 要增加几(大于0)* @return*/public static long incr(String key, long delta) {if (delta < 0) {throw new BusinessException("递增因子必须大于0");}return RedisUtil.getInstance().opsForValue().increment(key, delta);}/*** 递减* @param key 键* @param delta 要减少几(小于0)* @return*/public static long decr(String key, long delta) {if (delta < 0) {throw new BusinessException("递减因子必须大于0");}return RedisUtil.getInstance().opsForValue().increment(key, -delta);}/*** 判断key是否存在* @param key 键* @return true 存在 false不存在*/public static boolean hasKey(String key) {try {return RedisUtil.getInstance().hasKey(key);} catch (Exception e) {log.error(e.getMessage(), e);return false;}}
}
@Slf4j
public class SecurityUtils {private SecurityUtils() {}/*** 获取用户账户**/public static Long getUserId() {try {LoginUser loginUser = getLoginUser();if(loginUser != null){return loginUser.getUserId();}else{return null;}} catch (Exception e) {throw new BusinessException("获取用户账户异常", HttpStatus.UNAUTHORIZED.value());}}/*** 获取用户账户**/public static String getUsername() {try {LoginUser loginUser = getLoginUser();if(loginUser != null){return loginUser.getUsername();}else{return "";}} catch (Exception e) {throw new BusinessException("获取用户账户异常", HttpStatus.UNAUTHORIZED.value());}}/*** 获取用户**/public static LoginUser getLoginUser() {try {Object principal = getAuthentication().getPrincipal();if (principal instanceof LoginUser) {return (LoginUser) principal;}return null;} catch (Exception e) {log.error("获取用户信息异常", e);return null;}}/*** 获取Authentication*/public static Authentication getAuthentication() {return SecurityContextHolder.getContext().getAuthentication();}/*** 生成BCryptPasswordEncoder密码** @param password 密码* @return 加密字符串*/public static String encryptPassword(String password) {BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();return passwordEncoder.encode(password);}/*** 判断密码是否相同** @param rawPassword     真实密码* @param encodedPassword 加密后字符* @return 结果*/public static boolean matchesPassword(String rawPassword, String encodedPassword) {BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();return passwordEncoder.matches(rawPassword, encodedPassword);}/*** 是否为管理员** @param userId 用户ID* @return 结果*/public static boolean isAdmin(Long userId) {return userId != null && 1L == userId;}
}
@Component
public class SpringUtils implements ApplicationContextAware {private static ApplicationContext applicationContext;public static ApplicationContext getApplicationContext() {return applicationContext;}@Overridepublic void setApplicationContext(@NonNull ApplicationContext applicationContext) {SpringUtils.applicationContext = applicationContext;}public static Object getBean(String name) {return applicationContext.getBean(name);}public static <T> T getBean(Class<T> requiredType) {return applicationContext.getBean(requiredType);}public static <T> T getBean(String name, Class<T> requiredType) {return applicationContext.getBean(name, requiredType);}public static boolean containsBean(String name) {return applicationContext.containsBean(name);}public static boolean isSingleton(String name) {return applicationContext.isSingleton(name);}public static Class<?> getType(String name) {return applicationContext.getType(name);}/*** 获取当前环境** @return String*/public static String getActiveProfile() {String[] activeProfiles = getActiveProfiles();return ArrayUtil.isNotEmpty(activeProfiles) ? activeProfiles[0] : null;}/*** 获取当前环境** @return String[]*/public static String[] getActiveProfiles() {return applicationContext.getEnvironment().getActiveProfiles();}}

spring-security 实现单点登录相关推荐

  1. Spring Security OAuth2 单点登录和登出

    文章目录 1. 单点登录 1.1 使用内存保存客户端和用户信息 1.1.1 认证中心 auth-server 1.1.2 子系统 service-1 1.1.3 测试 1.2 使用数据库保存客户端和用 ...

  2. Spring Security Oauth2 单点登录案例实现和执行流程剖析

    我已经试过了 教程很完美 Spring Security Oauth2 OAuth是一个关于授权的开放网络标准,在全世界得到的广泛的应用,目前是2.0的版本.OAuth2在"客户端" ...

  3. Spring Security OAuth2 单点登录

    1. 概述 在前面的文章中,我们学习了 Spring Security OAuth 的简单使用. <Spring Security OAuth2 入门> <Spring Securi ...

  4. Spring Boot 实现单点登录的第三种方案!

    前面松哥发过两篇文章,也是两种方案,讲到单点登录问题: OAuth2+JWT 方案 @EnableOAuth2Sso 注解方案 今天再来和大家介绍第三种方案,使用 Spring Security 开发 ...

  5. spring security 自定义认证登录

    spring security 自定义认证登录 1.概要 1.1.简介 spring security是一种基于 Spring AOP 和 Servlet 过滤器的安全框架,以此来管理权限认证等. 1 ...

  6. Spring Security 3 Ajax登录–访问受保护的资源

    我看过一些有关Spring Security 3 Ajax登录的博客,但是我找不到解决如何调用基于Ajax的登录的博客,匿名用户正在Ajax中访问受保护的资源. 问题 – Web应用程序允许匿名访问某 ...

  7. Spring Security OAuth2 实现登录互踢

    Spring Security OAuth2 实现登录互踢 工作中遇到的问题,通过网上查找资料,解决问题,记录一下,防止丢失. 1.重写DefaultTokenServices中的方法 ​ 自定义一个 ...

  8. spring security webflux 自定义登录页面

    spring security webflux 自定义登录页面 ************************* 相关类及接口 ServerHttpSecurity public class Ser ...

  9. 详解Spring Security的formLogin登录认证模式

    详解Spring Security的formLogin登录认证模式 一.formLogin的应用场景 在本专栏之前的文章中,已经给大家介绍过Spring Security的HttpBasic模式,该模 ...

  10. oauth2 单点登录_Spring Security Oauth2和Spring Boot实现单点登录

    最近在学习单点登录相关,调查了一下目前流行的单点登录解决方案:cas 和 oauth2,文章主要介绍oauth2 单点登录.希望这篇文章能帮助大家学习相关内容. 我们将使用两个单独的应用程序: 授权服 ...

最新文章

  1. 在CentOS 6.8 x86_64上利用devtoolset搭建GCC 4.9.2和5.3.1开发环境
  2. 阿里云凌晨大规模宕机,华北部分网站陷入瘫痪
  3. hibernate连接池配置
  4. 【NOIP2016】蚯蚓 --队列模拟
  5. 【转】一步步构建大型网站架构
  6. 自动化测试在CI CD管道中的作用
  7. Java虚拟机笔记(五):JVM中对象的分代
  8. 7-25 总结 Junit 测试 和断言 /ArrayList 和LinkedList 的区别/HashCode用来存放数据.
  9. 电信光纤友华PT921G,烽火HG220光猫破解关闭自带路由改桥接拨号教程
  10. Windows11中文用户名问题:适合于新电脑
  11. 微信小程序开发:绑定手机号获取验证码功能
  12. 5类6类7类网线对比_五类网线、六类网线和七类网线有什么区别?如何挑选网线?...
  13. drupal php 版本,纯PHP drupal主题
  14. 洪恩软件进军网游产业 池宇峰揭密完美世界
  15. 由access key泄露浅谈云安全
  16. Scratch少儿编程——豆腐女孩
  17. Android自定义View2--触摸事件传递机制
  18. Ubuntu使用git更新本地代码到github
  19. 【金仓数据库设置主键自增】
  20. 目的地址,源地址防火墙双向nat转换

热门文章

  1. 《金山词霸2009 牛津版》插件工具加载
  2. ubuntu添加桌面快捷方式图标
  3. Linux环境变量PATH
  4. notimplementedexception
  5. 13.SpringBoot学习(十三)——JDBC之 Spring Boot Jpa多数据源
  6. 未转变者临时服务器怎么开启,未转变者怎么开服务器 未转变者怎么创建服务器...
  7. 天下一品茗介绍:小户赛茶叶的特点是什么
  8. $timeout、$interval和$watch用法
  9. python 验证码识别
  10. [极客大挑战 2019]Http 1(修改HTTP请求包)