diff --git a/summer-ospp/2023/pig/Pig整合MaxKey集成指南.md b/summer-ospp/2023/pig/Pig整合MaxKey集成指南.md new file mode 100644 index 000000000..44c704bfb --- /dev/null +++ b/summer-ospp/2023/pig/Pig整合MaxKey集成指南.md @@ -0,0 +1,2222 @@ +# Pig整合MaxKey流程整理 + +## 主要工作介紹 + +1.pig集成maxkey中CAS的单点登录 +2.pig集成maxke的组织架构信息等 + +## pig介绍 + +### pig版本 + +#### pig后端版本:3.6 + +gitee地址:https://gitee.com/log4j/pig.git + +#### pig前端版本:最新代码 + +gitee地址:https://gitee.com/log4j/pig-ui.git + +## 流程梳理 + +### 1.pig-auth模块 + +#### 1.pom.xml的更改覆盖了pig的Oauth2的token生成方案 + +```xml + + + + + 4.0.0 + + com.pig4cloud + pig + 3.6.7 + + + pig-auth + jar + + pig 认证授权中心,基于 spring security oAuth2 + + + + io.jsonwebtoken + jjwt + 0.7.0 + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-config + + + + com.pig4cloud + pig-common-feign + + + + com.pig4cloud + pig-upms-api + + + com.pig4cloud + pig-common-security + + + org.springframework.boot + spring-boot-starter-security + + + + org.springframework.boot + spring-boot-starter-freemarker + + + + org.springframework.boot + spring-boot-starter-undertow + + + + com.pig4cloud + pig-common-log + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + io.fabric8 + docker-maven-plugin + + + + + + +``` + +#### PigTokenEndpoint中新增获取token的方法 + +```java +/* + * Copyright (c) 2020 pig4cloud Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.pig4cloud.pig.auth.endpoint; + +import cn.hutool.core.date.DatePattern; +import cn.hutool.core.date.TemporalAccessorUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.pig4cloud.pig.admin.api.entity.SysOauthClientDetails; +import com.pig4cloud.pig.admin.api.feign.RemoteClientDetailsService; +import com.pig4cloud.pig.admin.api.vo.TokenVo; +import com.pig4cloud.pig.auth.support.handler.PigAuthenticationFailureEventHandler; +import com.pig4cloud.pig.auth.utils.RedisUtils; +import com.pig4cloud.pig.auth.utils.TokenManager; +import com.pig4cloud.pig.common.core.constant.CacheConstants; +import com.pig4cloud.pig.common.core.constant.CommonConstants; +import com.pig4cloud.pig.common.core.util.R; +import com.pig4cloud.pig.common.core.util.RetOps; +import com.pig4cloud.pig.common.core.util.SpringContextHolder; +import com.pig4cloud.pig.common.security.annotation.Inner; +import com.pig4cloud.pig.common.security.service.PigUser; +import com.pig4cloud.pig.common.security.util.OAuth2EndpointUtils; +import com.pig4cloud.pig.common.security.util.OAuth2ErrorCodesExpand; +import com.pig4cloud.pig.common.security.util.OAuthClientException; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.CacheManager; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.server.ServletServerHttpResponse; +import org.springframework.security.authentication.event.LogoutSuccessEvent; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter; +import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; +import org.springframework.security.oauth2.server.authorization.OAuth2TokenType; +import org.springframework.security.oauth2.server.resource.InvalidBearerTokenException; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.ModelAndView; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.security.Principal; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * @author lengleng + * @date 2019/2/1 删除token端点 + */ +@Slf4j +@RestController +@RequiredArgsConstructor +@RequestMapping("/token") +public class PigTokenEndpoint { + + private final HttpMessageConverter accessTokenHttpResponseConverter = new OAuth2AccessTokenResponseHttpMessageConverter(); + + private final AuthenticationFailureHandler authenticationFailureHandler = new PigAuthenticationFailureEventHandler(); + + private final OAuth2AuthorizationService authorizationService; + + private final RemoteClientDetailsService clientDetailsService; + + private final RedisTemplate redisTemplate; + + private final CacheManager cacheManager; + + @Resource + private RedisUtils redisUtils; + + + @Resource + private TokenManager tokenManager; + + private final static String SPRING_SESSION_PREFIX = "spring:session:sessions:%s"; + private final static String PIG_TOKEN_PREFIX = "pig:token:%s:%s"; + private final static String ASSCEE_TOKEN = "access_token"; + private final static String REFRESH_TOKEN = "refresh_token"; + + private long tokenExpiration = 24 * 60 * 60 * 1000; + + + /** + * 认证页面 + * + * @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; + } + + @GetMapping("/confirm_access") + public ModelAndView confirm(Principal principal, ModelAndView modelAndView, + @RequestParam(OAuth2ParameterNames.CLIENT_ID) String clientId, + @RequestParam(OAuth2ParameterNames.SCOPE) String scope, + @RequestParam(OAuth2ParameterNames.STATE) String state) { + SysOauthClientDetails clientDetails = RetOps.of(clientDetailsService.getClientDetailsById(clientId)) + .getData() + .orElseThrow(() -> new OAuthClientException("clientId 不合法")); + + Set authorizedScopes = StringUtils.commaDelimitedListToSet(clientDetails.getScope()); + modelAndView.addObject("clientId", clientId); + modelAndView.addObject("state", state); + modelAndView.addObject("scopeList", authorizedScopes); + modelAndView.addObject("principalName", principal.getName()); + modelAndView.setViewName("ftl/confirm"); + return modelAndView; + } + + /** + * 退出并删除token + * + * @param authHeader Authorization + */ + @DeleteMapping("/logout") + public R logout(HttpServletRequest request, @RequestHeader(value = HttpHeaders.AUTHORIZATION, required = false) String authHeader) { + if (StrUtil.isBlank(authHeader)) { + return R.ok(); + } + String sessonId = request.getSession().getId(); + if (StrUtil.isBlank(sessonId)) { + return R.ok(); + } + boolean isSuccess = redisUtils.deleteKey(generateSessionId(sessonId)); + if (isSuccess) { + return R.ok(); + } else { + return R.failed(); + } + + } + + /** + * 校验token + * + * @param token 令牌 + */ + @SneakyThrows + @GetMapping("/check_token") + public void checkToken(String token, HttpServletResponse response, HttpServletRequest request) { + + ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response); + + if (StrUtil.isBlank(token)) { + httpResponse.setStatusCode(HttpStatus.UNAUTHORIZED); + this.authenticationFailureHandler.onAuthenticationFailure(request, response, + new InvalidBearerTokenException(OAuth2ErrorCodesExpand.TOKEN_MISSING)); + return; + } + OAuth2Authorization authorization = authorizationService.findByToken(token, OAuth2TokenType.ACCESS_TOKEN); + + // 如果令牌不存在 返回401 + if (authorization == null || authorization.getAccessToken() == null) { + this.authenticationFailureHandler.onAuthenticationFailure(request, response, + new InvalidBearerTokenException(OAuth2ErrorCodesExpand.INVALID_BEARER_TOKEN)); + return; + } + + Map claims = authorization.getAccessToken().getClaims(); + OAuth2AccessTokenResponse sendAccessTokenResponse = OAuth2EndpointUtils.sendAccessTokenResponse(authorization, + claims); + this.accessTokenHttpResponseConverter.write(sendAccessTokenResponse, MediaType.APPLICATION_JSON, httpResponse); + } + + /** + * 令牌管理调用 + * + * @param token token + */ + @Inner + @DeleteMapping("/{token}") + public R removeToken(@PathVariable("token") String token) { + OAuth2Authorization authorization = authorizationService.findByToken(token, OAuth2TokenType.ACCESS_TOKEN); + if (authorization == null) { + return R.ok(); + } + + OAuth2Authorization.Token accessToken = authorization.getAccessToken(); + if (accessToken == null || StrUtil.isBlank(accessToken.getToken().getTokenValue())) { + return R.ok(); + } + // 清空用户信息 + cacheManager.getCache(CacheConstants.USER_DETAILS).evict(authorization.getPrincipalName()); + // 清空access token + authorizationService.remove(authorization); + // 处理自定义退出事件,保存相关日志 + SpringContextHolder.publishEvent(new LogoutSuccessEvent(new PreAuthenticatedAuthenticationToken( + authorization.getPrincipalName(), authorization.getRegisteredClientId()))); + return R.ok(); + } + + /** + * 查询token + * + * @param params 分页参数 + * @return + */ + @Inner + @PostMapping("/page") + public R tokenList(@RequestBody Map params) { + // 根据分页参数获取对应数据 + String key = String.format("%s::*", CacheConstants.PROJECT_OAUTH_ACCESS); + int current = MapUtil.getInt(params, CommonConstants.CURRENT); + int size = MapUtil.getInt(params, CommonConstants.SIZE); + Set keys = redisTemplate.keys(key); + List pages = keys.stream().skip((current - 1) * size).limit(size).collect(Collectors.toList()); + Page result = new Page(current, size); + + List tokenVoList = redisTemplate.opsForValue().multiGet(pages).stream().map(obj -> { + OAuth2Authorization authorization = (OAuth2Authorization) obj; + TokenVo tokenVo = new TokenVo(); + tokenVo.setClientId(authorization.getRegisteredClientId()); + tokenVo.setId(authorization.getId()); + tokenVo.setUsername(authorization.getPrincipalName()); + OAuth2Authorization.Token accessToken = authorization.getAccessToken(); + tokenVo.setAccessToken(accessToken.getToken().getTokenValue()); + + String expiresAt = TemporalAccessorUtil.format(accessToken.getToken().getExpiresAt(), + DatePattern.NORM_DATETIME_PATTERN); + tokenVo.setExpiresAt(expiresAt); + + String issuedAt = TemporalAccessorUtil.format(accessToken.getToken().getIssuedAt(), + DatePattern.NORM_DATETIME_PATTERN); + tokenVo.setIssuedAt(issuedAt); + return tokenVo; + }).collect(Collectors.toList()); + result.setRecords(tokenVoList); + result.setTotal(keys.size()); + return R.ok(result); + } + + @GetMapping("sso_login_get_token") + public R> getToken(String ticket, String service) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + PigUser pigUser = (PigUser) authentication.getPrincipal(); + Map ans = new HashMap<>(); + String access_token = tokenManager.createToken(ASSCEE_TOKEN, pigUser.getName(), pigUser.getId().toString()); + String refresh_token = tokenManager.createToken(REFRESH_TOKEN, pigUser.getName(), pigUser.getId().toString()); + redisUtils.setValue(generateTokenKey(ASSCEE_TOKEN, pigUser.getId().toString()), access_token, tokenExpiration); + redisUtils.setValue(generateTokenKey(REFRESH_TOKEN, pigUser.getId().toString()), refresh_token, tokenExpiration); + ans.put("access_token", access_token); + ans.put("refresh_token", refresh_token); + return R.ok(ans); + } + + + private String generateSessionId(String sessionId) { + return String.format(SPRING_SESSION_PREFIX, sessionId); + } + + private String generateTokenKey(String type, String userId) { + return String.format(PIG_TOKEN_PREFIX, type, userId); + } + + +} + +``` + +#### 3.新增utils类 + +##### RedisUtils中操作缓存的工具类 + +```java +package com.pig4cloud.pig.auth.utils; + +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +@Component +public class RedisUtils { + + @Resource + private RedisTemplate redisTemplate; + + + public boolean deleteKey(String key) { + return redisTemplate.delete(key); + } + + public void setValue(String key, Object object, Long expire) { + redisTemplate.opsForValue().set(key, object, expire); + } + +} + +``` + +##### TokenMananer工具类 + +```java +package com.pig4cloud.pig.auth.utils; + +import io.jsonwebtoken.CompressionCodecs; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import org.springframework.stereotype.Component; + +import java.util.Date; + +@Component +public class TokenManager { + private long tokenExpiration = 24 * 60 * 60 * 1000; + private final static String TOKEN_SIGN_KEY = "MAKKEY_PIG"; + + public String createToken(String subject, String username, String id) { + String token = Jwts.builder() + .setSubject(subject) + .claim("nickname", username) + .claim("id", id) + .setExpiration(new Date(System.currentTimeMillis() + tokenExpiration)) + .signWith(SignatureAlgorithm.HS512, TOKEN_SIGN_KEY) + .compressWith(CompressionCodecs.GZIP).compact(); + return token; + } + + + public String getUserFromToken(String token) { + String user = Jwts.parser().setSigningKey(TOKEN_SIGN_KEY).parseClaimsJws(token).getBody().getSubject(); + return user; + } + + public void removeToken(String token) { + //jwttoken无需删除,客户端扔掉即可。 + } + +} + +``` + +### 2.pig-common + +#### pom文件的更改 + +主要新增 CAS的MAVEN包 + +```xml + + + + + 4.0.0 + + com.pig4cloud + pig-common + 3.6.7 + + + pig-common-security + jar + + pig 安全工具类 + + + + + org.springframework.session + spring-session-data-redis + + + + org.springframework.security + spring-security-cas + + + + com.pig4cloud + pig-common-core + + + cn.hutool + hutool-extra + + + + com.pig4cloud + pig-upms-api + + + + org.springframework.cloud + spring-cloud-commons + + + + org.springframework.cloud + spring-cloud-starter-openfeign + + + org.springframework.security + spring-security-oauth2-jose + + + org.springframework.security + spring-security-oauth2-authorization-server + ${spring.authorization.version} + + + org.springframework + spring-webmvc + + + + +``` + +#### annotation包下面 + +更改EnablePigResourceServer + +```java +/* + * Copyright (c) 2020 pig4cloud Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.pig4cloud.pig.common.security.annotation; + +import com.pig4cloud.pig.common.security.component.PigResourceServerAutoConfiguration; +import com.pig4cloud.pig.common.security.component.PigResourceServerConfiguration; +import com.pig4cloud.pig.common.security.component.ResourceAuthExceptionEntryPoint; +import com.pig4cloud.pig.common.security.config.*; +import org.springframework.context.annotation.Import; + +import java.lang.annotation.*; + +/** + * @author lengleng + * @date 2022-06-04 + *

+ * 资源服务注解 + */ +@Documented +@Inherited +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +//@Import({ PigResourceServerAutoConfiguration.class, PigResourceServerConfiguration.class }) +@Import({PigResourceServerAutoConfiguration.class, CasProperties.class, SecurityConfig.class}) +public @interface EnablePigResourceServer { + +} + +``` + +#### Config包下 + +##### 新增CasProperties主要是CAS得配置信息配置在NACOS中 + +```java +package com.pig4cloud.pig.common.security.config; + +import lombok.Data; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Data +@Component +public class CasProperties { + + /** + * 秘钥 + */ + @Value("${cas.key}") + private String casKey; + + /** + * cas服务端地址 + */ + @Value("${cas.server.host.url}") + private String casServerUrl; + + /** + * cas服务端地址 + */ + @Value("${cas.server.host.grant_url}") + private String casGrantingUrl; + + /** + * cas服务端登录地址 + */ + @Value("${cas.server.host.login_url}") + private String casServerLoginUrl; + + /** + * cas服务端登出地址 并回跳到制定页面 + */ + @Value("${cas.server.host.logout_url}") + private String casServerLogoutUrl; + + /** + * cas客户端地址 + */ + @Value("${cas.service.host.url}") + private String casServiceUrl; + + /** + * cas客户端地址登录地址 + */ + @Value("${cas.service.host.login_url}") + private String casServiceLoginUrl; + + /** + * cas客户端地址登出地址 + */ + @Value("${cas.service.host.logout_url}") + private String casServiceLogoutUrl; + +} + +``` + +##### SecurityConfig类主要是CAS的认证流程并且覆盖原本pig的认证流程 + +```java +package com.pig4cloud.pig.common.security.config; + +import cn.hutool.core.util.ArrayUtil; +import com.pig4cloud.pig.common.security.component.PermitAllUrlProperties; +import com.pig4cloud.pig.common.security.component.PigBearerTokenExtractor; +import com.pig4cloud.pig.common.security.component.ResourceAuthExceptionEntryPoint; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.jasig.cas.client.session.SingleSignOutFilter; +import org.jasig.cas.client.validation.Cas20ServiceTicketValidator; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.cas.ServiceProperties; +import org.springframework.security.cas.authentication.CasAuthenticationProvider; +import org.springframework.security.cas.web.CasAuthenticationEntryPoint; +import org.springframework.security.cas.web.CasAuthenticationFilter; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.core.userdetails.AuthenticationUserDetailsService; +import org.springframework.security.web.authentication.logout.LogoutFilter; +import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler; + +@Slf4j +@Configuration +@EnableWebSecurity // 启用web权限 +@EnableGlobalMethodSecurity(prePostEnabled = true) // 启用方法验证 +@RequiredArgsConstructor +public class SecurityConfig extends WebSecurityConfigurerAdapter { + + @Autowired + private CasProperties casProperties; + + @Autowired + private AuthenticationUserDetailsService casUserDetailService; + + private final PermitAllUrlProperties permitAllUrl; + + + /** + * 定义认证用户信息获取来源,密码校验规则等 + */ + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + super.configure(auth); + auth.authenticationProvider(casAuthenticationProvider()); + } + + /** + * 定义安全策略 + */ + @Override + protected void configure(HttpSecurity http) throws Exception { + + http.authorizeRequests()// 配置安全策略 + .antMatchers(ArrayUtil.toArray(permitAllUrl.getUrls(), String.class)).permitAll() + .anyRequest().authenticated()// 其余的所有请求都需要验证 + .and().logout().permitAll()// 定义logout不需要验证 + .and().formLogin();// 使用form表单登录 + + http.exceptionHandling() + .authenticationEntryPoint(casAuthenticationEntryPoint()) + .and() + .addFilter(casAuthenticationFilter()) + .addFilterBefore(casLogoutFilter(), LogoutFilter.class) + .addFilterBefore(singleSignOutFilter(), CasAuthenticationFilter.class); + // 取消跨站请求伪造防护 + http.csrf().disable(); +// // 防止iframe 造成跨域 + http.headers().frameOptions().disable(); + // http.csrf().disable(); //禁用CSRF + } + + /** + * 认证的入口 + */ + @Bean + public CasAuthenticationEntryPoint casAuthenticationEntryPoint() { + CasAuthenticationEntryPoint casAuthenticationEntryPoint = new CasAuthenticationEntryPoint(); + casAuthenticationEntryPoint.setLoginUrl(casProperties.getCasServerLoginUrl()); + casAuthenticationEntryPoint.setServiceProperties(serviceProperties()); + return casAuthenticationEntryPoint; + } + + /** + * 指定service相关信息 + */ + @Bean + public ServiceProperties serviceProperties() { + ServiceProperties serviceProperties = new ServiceProperties(); + //设置cas客户端登录完整的url + serviceProperties.setService(casProperties.getCasServiceUrl() + casProperties.getCasServiceLoginUrl()); + serviceProperties.setSendRenew(false); + serviceProperties.setAuthenticateAllArtifacts(true); + return serviceProperties; + } + + /** + * CAS认证过滤器 + */ + @Bean + public CasAuthenticationFilter casAuthenticationFilter() throws Exception { + CasAuthenticationFilter casAuthenticationFilter = new CasAuthenticationFilter(); + casAuthenticationFilter.setAuthenticationManager(authenticationManager()); + casAuthenticationFilter.setFilterProcessesUrl(casProperties.getCasServiceUrl() + casProperties.getCasServiceLoginUrl()); + casAuthenticationFilter.setServiceProperties(serviceProperties()); + return casAuthenticationFilter; + } + + /** + * cas 认证 Provider + */ + @Bean + public CasAuthenticationProvider casAuthenticationProvider() { + CasAuthenticationProvider casAuthenticationProvider = new CasAuthenticationProvider(); + casAuthenticationProvider.setAuthenticationUserDetailsService(casUserDetailService); + // //这里只是接口类型,实现的接口不一样,都可以的。 + casAuthenticationProvider.setServiceProperties(serviceProperties()); + casAuthenticationProvider.setTicketValidator(cas20ServiceTicketValidator()); + casAuthenticationProvider.setKey("casAuthenticationProviderKey"); + return casAuthenticationProvider; + } + + + + + @Bean + public Cas20ServiceTicketValidator cas20ServiceTicketValidator() { + return new Cas20ServiceTicketValidator(casProperties.getCasGrantingUrl()); + } + + /** + * 单点登出过滤器 + */ + @Bean + public SingleSignOutFilter singleSignOutFilter() { + SingleSignOutFilter singleSignOutFilter = new SingleSignOutFilter(); + singleSignOutFilter.setLogoutCallbackPath(casProperties.getCasServerUrl()); + singleSignOutFilter.setIgnoreInitConfiguration(true); + return singleSignOutFilter; + } + + /** + * 请求单点退出过滤器 + */ + @Bean + public LogoutFilter casLogoutFilter() { + LogoutFilter logoutFilter = new LogoutFilter(casProperties.getCasServerLogoutUrl(), new SecurityContextLogoutHandler()); + logoutFilter.setFilterProcessesUrl(casProperties.getCasServiceLogoutUrl()); + return logoutFilter; + } +} + +``` + +#### service包 + +新增PigUserDetailsServiceImpl类主要功能为CAS服务认证成功方法,判断用户是否存在,存在获取用户信息,不存在调用远程接口新增用户并同步组织架构信息 + +```java +/* + * Copyright (c) 2020 pig4cloud Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.pig4cloud.pig.common.security.service; + +import com.pig4cloud.pig.admin.api.dto.UserInfo; +import com.pig4cloud.pig.admin.api.feign.RemoteUserService; +import com.pig4cloud.pig.common.core.constant.CacheConstants; +import com.pig4cloud.pig.common.core.util.R; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.context.annotation.Primary; +import org.springframework.security.cas.authentication.CasAssertionAuthenticationToken; +import org.springframework.security.core.userdetails.AuthenticationUserDetailsService; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UsernameNotFoundException; + +import java.util.*; + +/** + * 用户详细信息 + * + * @author lengleng hccake + */ +@Slf4j +@Primary +@RequiredArgsConstructor +public class PigUserDetailsServiceImpl implements PigUserDetailsService, AuthenticationUserDetailsService { + + private final RemoteUserService remoteUserService; + + private final CacheManager cacheManager; + + /** + * 用户名密码登录 + * + * @param username 用户名 + * @return + */ + @Override + @SneakyThrows + public UserDetails loadUserByUsername(String username) { + Cache cache = cacheManager.getCache(CacheConstants.USER_DETAILS); + if (cache != null && cache.get(username) != null) { + return (PigUser) cache.get(username).get(); + } + return getUserDetails(remoteUserService.info(username), username); + } + + @Override + public int getOrder() { + return Integer.MIN_VALUE; + } + + @Override + public UserDetails loadUserDetails(CasAssertionAuthenticationToken token) throws UsernameNotFoundException { + log.info("getCredentials:{}", token.getCredentials()); + String username = token.getName(); + Cache cache = cacheManager.getCache(CacheConstants.USER_DETAILS); + if (cache != null && cache.get(username) != null) { + return (PigUser) cache.get(username).get(); + } + R result = remoteUserService.saveIfNotExist(token.getAssertion().getPrincipal().getAttributes()); + return getUserDetails(result, username); + } + + private UserDetails getUserDetails(R result, String username) { + Cache cache = cacheManager.getCache(CacheConstants.USER_DETAILS); + UserDetails userDetails = getUserDetails(result); + if (cache != null) { + cache.put(username, userDetails); + } + return userDetails; + } + + +} + +``` + +### pig-upms包 + +#### pom的更改 + +新增guava工具类 + +```java + + + + + 4.0.0 + + com.pig4cloud + pig-upms + 3.6.7 + + + pig-upms-biz + jar + + pig 通用用户权限管理系统业务处理模块 + + + + com.google.guava + guava + 29.0-jre + + + + com.pig4cloud + pig-upms-api + + + + com.pig4cloud.plugin + oss-spring-boot-starter + + + + com.pig4cloud + pig-common-feign + + + + com.pig4cloud + pig-common-security + + + + com.pig4cloud + pig-common-log + + + + com.pig4cloud + pig-common-swagger + + + + com.baomidou + mybatis-plus-boot-starter + + + com.mysql + mysql-connector-j + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-config + + + + io.springboot.sms + aliyun-sms-spring-boot-starter + + + + com.pig4cloud + pig-common-xss + + + + org.springframework.boot + spring-boot-starter-undertow + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + io.fabric8 + docker-maven-plugin + + + + + src/main/resources + true + + **/*.xlsx + **/*.xls + + + + src/main/resources + false + + **/*.xlsx + **/*.xls + + + + + + + +``` + +#### controller包 + +在SysUserController中新增方法主要为提供远程调用方法,判断用户是否存在,添加用户信息等 + +```java +/* + * Copyright (c) 2020 pig4cloud Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.pig4cloud.pig.admin.controller; + +import cn.hutool.core.collection.CollUtil; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.google.common.collect.Lists; +import com.pig4cloud.pig.admin.api.dto.UserDTO; +import com.pig4cloud.pig.admin.api.dto.UserInfo; +import com.pig4cloud.pig.admin.api.entity.SysUser; +import com.pig4cloud.pig.admin.api.vo.UserExcelVO; +import com.pig4cloud.pig.admin.api.vo.UserInfoVO; +import com.pig4cloud.pig.admin.api.vo.UserVO; +import com.pig4cloud.pig.admin.service.SysUserService; +import com.pig4cloud.pig.admin.utils.BeanCreator; +import com.pig4cloud.pig.common.core.exception.ErrorCodes; +import com.pig4cloud.pig.common.core.util.MsgUtils; +import com.pig4cloud.pig.common.core.util.R; +import com.pig4cloud.pig.common.log.annotation.SysLog; +import com.pig4cloud.pig.common.security.annotation.Inner; +import com.pig4cloud.pig.common.security.util.SecurityUtils; +import com.pig4cloud.pig.common.xss.core.XssCleanIgnore; +import com.pig4cloud.plugin.excel.annotation.RequestExcel; +import com.pig4cloud.plugin.excel.annotation.ResponseExcel; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import org.springframework.beans.BeanUtils; +import org.springframework.http.HttpHeaders; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * @author lengleng + * @date 2019/2/1 + */ +@Slf4j +@RestController +@RequiredArgsConstructor +@RequestMapping("/user") +@Tag(name = "用户管理模块") +@SecurityRequirement(name = HttpHeaders.AUTHORIZATION) +public class SysUserController { + + private final SysUserService userService; + + /** + * 获取当前用户全部信息 + * + * @return 用户信息 + */ + @GetMapping(value = {"/info"}) + public R info() { + String username = SecurityUtils.getUser().getUsername(); + SysUser user = userService.getOne(Wrappers.query().lambda().eq(SysUser::getUsername, username)); + if (user == null) { + return R.failed(MsgUtils.getMessage(ErrorCodes.SYS_USER_QUERY_ERROR)); + } + UserInfo userInfo = userService.getUserInfo(user); + UserInfoVO vo = new UserInfoVO(); + vo.setSysUser(userInfo.getSysUser()); + vo.setRoles(userInfo.getRoles()); + vo.setPermissions(userInfo.getPermissions()); + return R.ok(vo); + } + + /** + * 获取指定用户全部信息 + * + * @return 用户信息 + */ + @Inner + @GetMapping("/info/{username}") + public R info(@PathVariable String username) { + SysUser user = userService.getOne(Wrappers.query().lambda().eq(SysUser::getUsername, username)); + if (user == null) { + return R.failed(MsgUtils.getMessage(ErrorCodes.SYS_USER_USERINFO_EMPTY, username)); + } + return R.ok(userService.getUserInfo(user)); + } + + @Inner + @PostMapping("/sso_save") + public R save_sso(@RequestBody Map attributes) { + String username = (String) attributes.getOrDefault("username", "pig"); + // 判断用户名是否存在 + SysUser sysUser = userService.getOne(Wrappers.lambdaQuery().eq(SysUser::getUsername, username)); + if (sysUser == null) { + SysUser sysUserByMap = BeanCreator.createSysUserByMap(attributes); + UserDTO userDTO = new UserDTO(); + BeanUtils.copyProperties(sysUserByMap,userDTO); + + userDTO.setPost(Lists.newArrayList(1l)); + userDTO.setDeptId(6l); + userDTO.setRole(Lists.newArrayList(2l)); + // 添加用户 + userService.saveUser(userDTO); + } + return info(username); + } + + /** + * 根据部门id,查询对应的用户 id 集合 + * + * @param deptIds 部门id 集合 + * @return 用户 id 集合 + */ + @Inner + @GetMapping("/ids") + public R> listUserIdByDeptIds(@RequestParam("deptIds") Set deptIds) { + return R.ok(userService.listUserIdByDeptIds(deptIds)); + } + + /** + * 通过ID查询用户信息 + * + * @param id ID + * @return 用户信息 + */ + @GetMapping("/{id:\\d+}") + public R user(@PathVariable Long id) { + return R.ok(userService.getUserVoById(id)); + } + + /** + * 判断用户是否存在 + * + * @param userDTO 查询条件 + * @return + */ + @Inner(false) + @GetMapping("/check/exist") + public R isExist(UserDTO userDTO) { + List sysUserList = userService.list(new QueryWrapper<>(userDTO)); + if (CollUtil.isNotEmpty(sysUserList)) { + return R.ok(Boolean.TRUE, MsgUtils.getMessage(ErrorCodes.SYS_USER_EXISTING)); + } + return R.ok(Boolean.FALSE); + } + + /** + * 删除用户信息 + * + * @param id ID + * @return R + */ + @SysLog("删除用户信息") + @DeleteMapping("/{id:\\d+}") + @PreAuthorize("@pms.hasPermission('sys_user_del')") + public R userDel(@PathVariable Long id) { + SysUser sysUser = userService.getById(id); + return R.ok(userService.removeUserById(sysUser)); + } + + /** + * 添加用户 + * + * @param userDto 用户信息 + * @return success/false + */ + @SysLog("添加用户") + @PostMapping + @XssCleanIgnore({"password"}) + @PreAuthorize("@pms.hasPermission('sys_user_add')") + public R user(@RequestBody UserDTO userDto) { + return R.ok(userService.saveUser(userDto)); + } + + /** + * 管理员更新用户信息 + * + * @param userDto 用户信息 + * @return R + */ + @SysLog("更新用户信息") + @PutMapping + @XssCleanIgnore({"password"}) + @PreAuthorize("@pms.hasPermission('sys_user_edit')") + public R updateUser(@Valid @RequestBody UserDTO userDto) { + return userService.updateUser(userDto); + } + + /** + * 分页查询用户 + * + * @param page 参数集 + * @param userDTO 查询参数列表 + * @return 用户集合 + */ + @GetMapping("/page") + public R> getUserPage(Page page, UserDTO userDTO) { + return R.ok(userService.getUserWithRolePage(page, userDTO)); + } + + /** + * 个人修改个人信息 + * + * @param userDto userDto + * @return success/false + */ + @SysLog("修改个人信息") + @PutMapping("/edit") + @XssCleanIgnore({"password", "newpassword1"}) + public R updateUserInfo(@Valid @RequestBody UserDTO userDto) { + userDto.setUsername(SecurityUtils.getUser().getUsername()); + return userService.updateUserInfo(userDto); + } + + /** + * @param username 用户名称 + * @return 上级部门用户列表 + */ + @GetMapping("/ancestor/{username}") + public R> listAncestorUsers(@PathVariable String username) { + return R.ok(userService.listAncestorUsersByUsername(username)); + } + + /** + * 导出excel 表格 + * + * @param userDTO 查询条件 + * @return + */ + @ResponseExcel + @GetMapping("/export") + @PreAuthorize("@pms.hasPermission('sys_user_import_export')") + public List export(UserDTO userDTO) { + return userService.listUser(userDTO); + } + + /** + * 导入用户 + * + * @param excelVOList 用户列表 + * @param bindingResult 错误信息列表 + * @return R + */ + @PostMapping("/import") + @PreAuthorize("@pms.hasPermission('sys_user_import_export')") + public R importUser(@RequestExcel List excelVOList, BindingResult bindingResult) { + return userService.importUser(excelVOList, bindingResult); + } + +} + +``` + +#### utils包 + +新增BeanCreator方法,主要创建SysUser对象工具类 + +```java +package com.pig4cloud.pig.admin.utils; + +import com.pig4cloud.pig.admin.api.entity.SysUser; + +import java.util.Map; + +public class BeanCreator { + + private static final String DEFAULT_PASSWORD = "pigmax123456"; + + public static SysUser createSysUserByMap(Map map) { + SysUser sysUser = new SysUser(); + String username = (String) map.get("username"); + String phone = (String) map.get("mobile"); + String deptId = (String) map.get("departmentId"); + sysUser.setUsername(username); + sysUser.setPhone(phone); + sysUser.setDeptId(Long.parseLong(deptId)); + sysUser.setPassword(DEFAULT_PASSWORD); + return sysUser; + } + +} + +``` + +### vue包 + +主要是对pig前端页面的修改 + +#### api包 + +新增获取token的方法 + +```js +/* + * 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) + */ +import { validatenull } from '@/util/validate' +import request from '@/router/axios' +import store from '@/store' +import qs from 'qs' +import { getStore, setStore } from '@/util/store.js' +import website from '@/config/website' + + +const scope = 'server' + +export const loginByUsername = (username, password, code, randomStr) => { + const grant_type = 'password' + const dataObj = qs.stringify({ 'username': username, 'password': password }) + + const basicAuth = 'Basic ' + window.btoa(website.formLoginClient) + + // 保存当前选中的 basic 认证信息 + setStore({ + name: 'basicAuth', + content: basicAuth, + type: 'session' + }) + + return request({ + url: '/auth/oauth2/token', + headers: { + isToken: false, + Authorization: basicAuth + }, + method: 'post', + params: { randomStr, code, grant_type, scope }, + data: dataObj + }) +} + +export const loginByMobile = (smsForm) => { + const grant_type = 'app' + + const basicAuth = 'Basic ' + window.btoa(website.smsLoginClient) + + // 保存当前选中的 basic 认证信息 + setStore({ + name: 'basicAuth', + content: basicAuth, + type: 'session' + }) + + return request({ + url: '/auth/oauth2/token', + headers: { + isToken: false, + 'Authorization': basicAuth + }, + method: 'post', + params: { phone: smsForm.phone, code: smsForm.code, grant_type, scope } + }) +} + +export const ssoLogin = (ticket,service) => { + return request({ + url: '/auth/token/sso_login_get_token', + method: 'get', + params: { ticket, service } + }) +} + +export const refreshToken = refresh_token => { + const grant_type = 'refresh_token' + // 获取当前选中的 basic 认证信息 + const basicAuth = getStore({ name: 'basicAuth' }) + + return request({ + url: '/auth/oauth2/token', + headers: { + isToken: false, + Authorization: basicAuth + }, + method: 'post', + params: { refresh_token, grant_type, scope } + }) +} + +export const getUserInfo = () => { + return request({ + url: '/admin/user/info', + method: 'get' + }) +} + +export const logout = () => { + return request({ + url: '/auth/token/logout', + method: 'delete' + }) +} + +/** + * 校验令牌,若有效期小于半小时自动续期 + * + * 定时任务请求后端接口返回实际的有效时间,不进行本地计算避免 客户端和服务器机器时钟不一致 + * @param refreshLock + */ +export const checkToken = (refreshLock, $store) => { + const token = store.getters.access_token + // 获取当前选中的 basic 认证信息 + const basicAuth = getStore({ name: 'basicAuth' }) + + if (validatenull(token) || validatenull(basicAuth)) { + return + } + + request({ + url: '/auth/token/check_token', + headers: { + isToken: false, + Authorization: basicAuth + }, + method: 'get', + params: { token } + }).then(response => { + const expire = response && response.data && response.data.exp + if (expire) { + const expiredPeriod = expire * 1000 - new Date().getTime() + console.log('当前token过期时间', expiredPeriod, '毫秒') + //小于半小时自动续约 + if (expiredPeriod <= website.remainingTime) { + if (!refreshLock) { + refreshLock = true + $store.dispatch('RefreshToken') + .catch(() => { + clearInterval(this.refreshTime) + }) + refreshLock = false + } + } + } + }).catch(error => { + console.error(error) + }) +} + +/** + * 注册用户 + */ +export const registerUser = (userInfo) => { + return request({ + url: '/admin/register/user', + method: 'post', + data: userInfo + }) +} + + +/** + * 发送短信 + */ +export const sendSmsCode = (form) => { + return request({ + url: '/admin/app/sms', + method: 'post', + data: form + }) +} + +``` + +#### userlogin改造 + +```html + + + + + + +``` + +#### router改造 + +如果没权限返回登录页面 + +```js + +import axios from 'axios' +import { serialize } from '@/util' +import NProgress from 'nprogress' // progress bar +import errorCode from '@/const/errorCode' +import { ElMessage, ElMessageBox } from 'element-plus' +import 'nprogress/nprogress.css' +import qs from 'qs' +import store from '@/store' +import router from '@/router/index.js' +import { baseUrl } from '@/config/env' // progress bar style +axios.defaults.timeout = 30000 +// 返回其他状态吗 +axios.defaults.validateStatus = function(status) { + return status >= 200 && status <= 500 // 默认的 +} +// 跨域请求,允许保存cookie +axios.defaults.withCredentials = true +// NProgress Configuration +NProgress.configure({ + showSpinner: false +}) + +// HTTPrequest拦截 +axios.defaults.baseURL = baseUrl +axios.interceptors.request.use(config => { + NProgress.start() // start progress bar + const isToken = (config.headers || {}).isToken === false + const token = store.getters.access_token + + if (token && !isToken) { + config.headers['Authorization'] = 'Bearer ' + token// token + } + + // headers中配置serialize为true开启序列化 + if (config.method === 'post' && config.headers.serialize) { + config.data = serialize(config.data) + delete config.data.serialize + } + + if (config.method === 'get') { + config.paramsSerializer = function(params) { + return qs.stringify(params, { arrayFormat: 'repeat' }) + } + } + return config +}, error => { + return Promise.reject(error) +}) + +// HTTPresponse拦截 +axios.interceptors.response.use(res => { + NProgress.done() + const status = Number(res.status) || 200 + const message = res.data.msg || errorCode[status] || errorCode['default'] + if (status == 401){ + window.open("http://localhost:3000/") + } + + // 后台定义 424 针对令牌过去的特殊响应码 + if (status === 424) { + ElMessageBox.confirm('令牌状态已过期,请点击重新登录', '系统提示', { + confirmButtonText: '重新登录', + cancelButtonText: '取消', + type: 'warning' + } + ).then(() => { + store.dispatch('LogOut').then(() => { + // 刷新登录页面,避免多次弹框 + window.location.reload() + }) + }).catch(() => { + }) + return + } + + if (status !== 200 || res.data.code === 1) { + ElMessage({ + message: message, + type: 'error' + }) + return Promise.reject(new Error(message)) + } + + return res +}, error => { + // 处理 503 网络异常 + console.log(error) + if (error.response.status === 503) { + ElMessage({ + message: error.response.data.msg, + type: 'error' + }) + } + NProgress.done() + return Promise.reject(new Error(error)) +}) + +export default axios + +``` + +#### store的user改造 + +```js +import { setToken, setRefreshToken } from '@/util/auth' +import { getStore, setStore } from '@/util/store' +import { ssoLogin,loginByMobile, loginByUsername, getUserInfo, logout, refreshToken } from '@/api/login' +import { deepClone, encryption } from '@/util' +import { formatPath } from '@/router/avue-router' +import { getMenu } from '@/api/admin/menu' +const user = { + state: { + userInfo: getStore({ + name: 'userInfo' + }) || {}, + permissions: getStore({ + name: 'permissions' + }) || [], + roles: [], + menu: getStore({ + name: 'menu' + }) || [], + menuAll: getStore({ name: 'menuAll' }) || [], + access_token: getStore({ + name: 'access_token' + }) || '', + refresh_token: getStore({ + name: 'refresh_token' + }) || '' + }, + actions: { + // SSO单点登陆 + SSOLogin({ commit }, myParam,service) { + return new Promise((resolve, reject) => { + ssoLogin(myParam,service).then(response => { + const data = response.data.data + console.log(data) + commit('SET_ACCESS_TOKEN', data.access_token) + commit('SET_REFRESH_TOKEN', data.refresh_token) + commit('CLEAR_LOCK') + resolve() + }).catch(error => { + reject(error) + }) + }) + }, + // 根据用户名登录 + LoginByUsername({ commit }, userInfo) { + const user = encryption({ + data: userInfo, + key: 'thanks,pig4cloud', + param: ['password'] + }) + return new Promise((resolve, reject) => { + loginByUsername(user.username, user.password, user.code, user.randomStr).then(response => { + const data = response.data + commit('SET_ACCESS_TOKEN', data.access_token) + commit('SET_REFRESH_TOKEN', data.refresh_token) + commit('CLEAR_LOCK') + resolve() + }).catch(error => { + reject(error) + }) + }) + }, + // 根据手机号登录 + LoginByPhone({ commit }, smsForm) { + return new Promise((resolve, reject) => { + loginByMobile(smsForm).then(response => { + const data = response.data + commit('SET_ACCESS_TOKEN', data.access_token) + commit('SET_REFRESH_TOKEN', data.refresh_token) + commit('CLEAR_LOCK') + resolve() + }).catch(error => { + reject(error) + }) + }) + }, + + // 刷新token + RefreshToken({ commit, state }) { + return new Promise((resolve, reject) => { + refreshToken(state.refresh_token).then(response => { + const data = response.data + commit('SET_ACCESS_TOKEN', data.access_token) + commit('SET_REFRESH_TOKEN', data.refresh_token) + commit('CLEAR_LOCK') + resolve() + }).catch(error => { + reject(error) + }) + }) + }, + // 查询用户信息 + GetUserInfo({ commit }) { + return new Promise((resolve, reject) => { + getUserInfo().then((res) => { + const data = res.data.data || {} + commit('SET_USER_INFO', data.sysUser) + commit('SET_ROLES', data.roles || []) + commit('SET_PERMISSIONS', data.permissions || []) + resolve(data) + }).catch(() => { + reject() + }) + }) + }, + // 登出 + LogOut({ commit }) { + return new Promise((resolve, reject) => { + logout().then(() => { + commit('SET_MENUALL_NULL', []) + commit('SET_MENU', []) + commit('SET_PERMISSIONS', []) + commit('SET_USER_INFO', {}) + commit('SET_ACCESS_TOKEN', '') + commit('SET_REFRESH_TOKEN', '') + commit('SET_ROLES', []) + commit('DEL_ALL_TAG') + commit('CLEAR_LOCK') + resolve() + }).catch(error => { + reject(error) + }) + }) + }, + // 注销session + FedLogOut({ commit }) { + return new Promise(resolve => { + commit('SET_MENU', []) + commit('SET_MENUALL_NULL', []) + commit('SET_PERMISSIONS', []) + commit('SET_USER_INFO', {}) + commit('SET_ACCESS_TOKEN', '') + commit('SET_REFRESH_TOKEN', '') + commit('SET_ROLES', []) + commit('DEL_ALL_TAG') + commit('CLEAR_LOCK') + resolve() + }) + }, + // 获取系统菜单 + GetMenu({ commit }, obj = {}) { + // 记录用户点击顶部信息,保证刷新的时候不丢失 + commit('LIKE_TOP_MENUID', obj) + return new Promise(resolve => { + getMenu(obj.id).then((res) => { + const data = res.data.data + const menu = deepClone(data) + menu.forEach(ele => formatPath(ele, true)) + commit('SET_MENUALL', menu) + commit('SET_MENU', menu) + resolve(menu) + }) + }) + }, + //顶部菜单 + GetTopMenu() { + return new Promise(resolve => { + resolve([]) + }) + } + }, + mutations: { + SET_ACCESS_TOKEN: (state, access_token) => { + state.access_token = access_token + setToken(access_token) + setStore({ + name: 'access_token', + content: state.access_token, + type: 'session' + }) + }, + SET_REFRESH_TOKEN: (state, rfToken) => { + state.refresh_token = rfToken + setRefreshToken(rfToken) + setStore({ + name: 'refresh_token', + content: state.refresh_token, + type: 'session' + }) + }, + SET_USER_INFO: (state, userInfo) => { + state.userInfo = userInfo + setStore({ + name: 'userInfo', + content: userInfo, + type: 'session' + }) + }, + SET_MENUALL: (state, menuAll) => { + const menu = state.menuAll + menuAll.forEach(ele => { + if (!menu.find(item => item.label === ele.label && item.path === ele.path)) { + menu.push(ele) + } + }) + state.menuAll = menu + setStore({ name: 'menuAll', content: state.menuAll }) + }, + SET_MENUALL_NULL: (state) => { + state.menuAll = [] + setStore({ name: 'menuAll', content: state.menuAll }) + }, + SET_MENU: (state, menu) => { + state.menu = menu + setStore({ name: 'menu', content: state.menu }) + }, + SET_ROLES: (state, roles) => { + state.roles = roles + }, + SET_PERMISSIONS: (state, permissions) => { + const list = {} + for (let i = 0; i < permissions.length; i++) { + list[permissions[i]] = true + } + + state.permissions = list + setStore({ + name: 'permissions', + content: list, + type: 'session' + }) + } + } + +} +export default user + +``` + +### Nacos配置文件新增 + +在application-dev.yml新增配置信息 + +```yml +# 配置文件加密根密码 +jasypt: + encryptor: + password: pig + algorithm: PBEWithMD5AndDES + iv-generator-classname: org.jasypt.iv.NoIvGenerator + +# Spring 相关 +spring: + cache: + type: redis + redis: + host: pig-redis + cloud: + sentinel: + eager: true + transport: + dashboard: pig-sentinel:5003 + +# 暴露监控端点 +management: + endpoints: + web: + exposure: + include: "*" + endpoint: + health: + show-details: ALWAYS + + +# feign 配置 +feign: + sentinel: + enabled: true + okhttp: + enabled: true + httpclient: + enabled: false + client: + config: + default: + connectTimeout: 10000 + readTimeout: 10000 + compression: + request: + enabled: true + response: + enabled: true + +# mybaits-plus配置 +mybatis-plus: + mapper-locations: classpath:/mapper/*Mapper.xml + global-config: + banner: false + db-config: + id-type: auto + table-underline: true + logic-delete-value: 1 + logic-not-delete-value: 0 + configuration: + map-underscore-to-camel-case: true + +# swagger 配置 +swagger: + enabled: true + title: Pig Swagger API + gateway: http://${GATEWAY_HOST:pig-gateway}:${GATEWAY-PORT:9999} + token-url: ${swagger.gateway}/auth/oauth2/token + scope: server + services: + pig-upms-biz: admin + pig-codegen: gen +#cas配置 +cas: + #秘钥 + key: n0c9MTcwMjIwMjMxNzE2NDMwOTAskV + server: + host: + grant_url: http://sso.maxkey.top/sign/authz/cas + #cas服务端地址 这是我的cas服务端地址 需要修改成你们的cas服务端地址 + url: http://sso.maxkey.top/maxkey/authz/cas + #cas服务端登录地址 + login_url: http://sso.maxkey.top/maxkey/#/passport/login?redirect_uri=aHR0cDovL3Nzby5tYXhrZXkudG9wL3NpZ24vYXV0aHovY2FzLzQxMDY1ZmUzLWFlNjctNDE3Mi1hNDYwLWZkMDA3OWU4ODI5NA + #cas服务端登出地址 service参数后面跟就是需要跳转的页面/接口 这里指定的是cas客户端登录接口 + logout_url: ${cas.server.host.url}/logout?service=${cas.service.host.url}${cas.service.host.login_url} + service: + host: + #cas客户端地址 + url: http://localhost:8080 + #cas客户端地址登录地址 + login_url: /login + #cas客户端地址登出地址 + logout_url: /logout +``` diff --git a/summer-ospp/2023/pig/Pig整合MaxKey集成指南.pdf b/summer-ospp/2023/pig/Pig整合MaxKey集成指南.pdf new file mode 100644 index 000000000..577674ab3 Binary files /dev/null and b/summer-ospp/2023/pig/Pig整合MaxKey集成指南.pdf differ diff --git a/summer-ospp/2023/pig/application-dev.yml b/summer-ospp/2023/pig/application-dev.yml new file mode 100644 index 000000000..2c27c76e1 --- /dev/null +++ b/summer-ospp/2023/pig/application-dev.yml @@ -0,0 +1,93 @@ +# 配置文件加密根密码 +jasypt: + encryptor: + password: pig + algorithm: PBEWithMD5AndDES + iv-generator-classname: org.jasypt.iv.NoIvGenerator + +# Spring 相关 +spring: + cache: + type: redis + redis: + host: pig-redis + cloud: + sentinel: + eager: true + transport: + dashboard: pig-sentinel:5003 + +# 暴露监控端点 +management: + endpoints: + web: + exposure: + include: "*" + endpoint: + health: + show-details: ALWAYS + + +# feign 配置 +feign: + sentinel: + enabled: true + okhttp: + enabled: true + httpclient: + enabled: false + client: + config: + default: + connectTimeout: 10000 + readTimeout: 10000 + compression: + request: + enabled: true + response: + enabled: true + +# mybaits-plus配置 +mybatis-plus: + mapper-locations: classpath:/mapper/*Mapper.xml + global-config: + banner: false + db-config: + id-type: auto + table-underline: true + logic-delete-value: 1 + logic-not-delete-value: 0 + configuration: + map-underscore-to-camel-case: true + +# swagger 配置 +swagger: + enabled: true + title: Pig Swagger API + gateway: http://${GATEWAY_HOST:pig-gateway}:${GATEWAY-PORT:9999} + token-url: ${swagger.gateway}/auth/oauth2/token + scope: server + services: + pig-upms-biz: admin + pig-codegen: gen +#cas配置 +cas: + #秘钥 + key: n0c9MTcwMjIwMjMxNzE2NDMwOTAskV + server: + host: + grant_url: http://sso.maxkey.top/sign/authz/cas + #cas服务端地址 这是我的cas服务端地址 需要修改成你们的cas服务端地址 + url: http://sso.maxkey.top/maxkey/authz/cas + #cas服务端登录地址 + login_url: http://sso.maxkey.top/maxkey/#/passport/login?redirect_uri=aHR0cDovL3Nzby5tYXhrZXkudG9wL3NpZ24vYXV0aHovY2FzLzQxMDY1ZmUzLWFlNjctNDE3Mi1hNDYwLWZkMDA3OWU4ODI5NA + #cas服务端登出地址 service参数后面跟就是需要跳转的页面/接口 这里指定的是cas客户端登录接口 + logout_url: ${cas.server.host.url}/logout?service=${cas.service.host.url}${cas.service.host.login_url} + service: + host: + #cas客户端地址 + url: http://localhost:8080 + #cas客户端地址登录地址 + login_url: /login + #cas客户端地址登出地址 + logout_url: /logout \ No newline at end of file diff --git a/summer-ospp/2023/pig/pig-auth/pom.xml b/summer-ospp/2023/pig/pig-auth/pom.xml new file mode 100644 index 000000000..eba9ac678 --- /dev/null +++ b/summer-ospp/2023/pig/pig-auth/pom.xml @@ -0,0 +1,96 @@ + + + + + 4.0.0 + + com.pig4cloud + pig + 3.6.7 + + + pig-auth + jar + + pig 认证授权中心,基于 spring security oAuth2 + + + + io.jsonwebtoken + jjwt + 0.7.0 + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-config + + + + com.pig4cloud + pig-common-feign + + + + com.pig4cloud + pig-upms-api + + + com.pig4cloud + pig-common-security + + + org.springframework.boot + spring-boot-starter-security + + + + org.springframework.boot + spring-boot-starter-freemarker + + + + org.springframework.boot + spring-boot-starter-undertow + + + + com.pig4cloud + pig-common-log + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + io.fabric8 + docker-maven-plugin + + + + + diff --git a/summer-ospp/2023/pig/pig-auth/src/main/java/com/pig4cloud/pig/auth/endpoint/PigTokenEndpoint.java b/summer-ospp/2023/pig/pig-auth/src/main/java/com/pig4cloud/pig/auth/endpoint/PigTokenEndpoint.java new file mode 100644 index 000000000..6ff908ea6 --- /dev/null +++ b/summer-ospp/2023/pig/pig-auth/src/main/java/com/pig4cloud/pig/auth/endpoint/PigTokenEndpoint.java @@ -0,0 +1,292 @@ +/* + * Copyright (c) 2020 pig4cloud Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.pig4cloud.pig.auth.endpoint; + +import cn.hutool.core.date.DatePattern; +import cn.hutool.core.date.TemporalAccessorUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.pig4cloud.pig.admin.api.entity.SysOauthClientDetails; +import com.pig4cloud.pig.admin.api.feign.RemoteClientDetailsService; +import com.pig4cloud.pig.admin.api.vo.TokenVo; +import com.pig4cloud.pig.auth.support.handler.PigAuthenticationFailureEventHandler; +import com.pig4cloud.pig.auth.utils.RedisUtils; +import com.pig4cloud.pig.auth.utils.TokenManager; +import com.pig4cloud.pig.common.core.constant.CacheConstants; +import com.pig4cloud.pig.common.core.constant.CommonConstants; +import com.pig4cloud.pig.common.core.util.R; +import com.pig4cloud.pig.common.core.util.RetOps; +import com.pig4cloud.pig.common.core.util.SpringContextHolder; +import com.pig4cloud.pig.common.security.annotation.Inner; +import com.pig4cloud.pig.common.security.service.PigUser; +import com.pig4cloud.pig.common.security.util.OAuth2EndpointUtils; +import com.pig4cloud.pig.common.security.util.OAuth2ErrorCodesExpand; +import com.pig4cloud.pig.common.security.util.OAuthClientException; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.CacheManager; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.server.ServletServerHttpResponse; +import org.springframework.security.authentication.event.LogoutSuccessEvent; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter; +import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; +import org.springframework.security.oauth2.server.authorization.OAuth2TokenType; +import org.springframework.security.oauth2.server.resource.InvalidBearerTokenException; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.ModelAndView; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.security.Principal; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * @author lengleng + * @date 2019/2/1 删除token端点 + */ +@Slf4j +@RestController +@RequiredArgsConstructor +@RequestMapping("/token") +public class PigTokenEndpoint { + + private final HttpMessageConverter accessTokenHttpResponseConverter = new OAuth2AccessTokenResponseHttpMessageConverter(); + + private final AuthenticationFailureHandler authenticationFailureHandler = new PigAuthenticationFailureEventHandler(); + + private final OAuth2AuthorizationService authorizationService; + + private final RemoteClientDetailsService clientDetailsService; + + private final RedisTemplate redisTemplate; + + private final CacheManager cacheManager; + + @Resource + private RedisUtils redisUtils; + + + @Resource + private TokenManager tokenManager; + + private final static String SPRING_SESSION_PREFIX = "spring:session:sessions:%s"; + private final static String PIG_TOKEN_PREFIX = "pig:token:%s:%s"; + private final static String ASSCEE_TOKEN = "access_token"; + private final static String REFRESH_TOKEN = "refresh_token"; + + private long tokenExpiration = 24 * 60 * 60 * 1000; + + + /** + * 认证页面 + * + * @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; + } + + @GetMapping("/confirm_access") + public ModelAndView confirm(Principal principal, ModelAndView modelAndView, + @RequestParam(OAuth2ParameterNames.CLIENT_ID) String clientId, + @RequestParam(OAuth2ParameterNames.SCOPE) String scope, + @RequestParam(OAuth2ParameterNames.STATE) String state) { + SysOauthClientDetails clientDetails = RetOps.of(clientDetailsService.getClientDetailsById(clientId)) + .getData() + .orElseThrow(() -> new OAuthClientException("clientId 不合法")); + + Set authorizedScopes = StringUtils.commaDelimitedListToSet(clientDetails.getScope()); + modelAndView.addObject("clientId", clientId); + modelAndView.addObject("state", state); + modelAndView.addObject("scopeList", authorizedScopes); + modelAndView.addObject("principalName", principal.getName()); + modelAndView.setViewName("ftl/confirm"); + return modelAndView; + } + + /** + * 退出并删除token + * + * @param authHeader Authorization + */ + @DeleteMapping("/logout") + public R logout(HttpServletRequest request, @RequestHeader(value = HttpHeaders.AUTHORIZATION, required = false) String authHeader) { + if (StrUtil.isBlank(authHeader)) { + return R.ok(); + } + String sessonId = request.getSession().getId(); + if (StrUtil.isBlank(sessonId)) { + return R.ok(); + } + boolean isSuccess = redisUtils.deleteKey(generateSessionId(sessonId)); + if (isSuccess) { + return R.ok(); + } else { + return R.failed(); + } + + } + + /** + * 校验token + * + * @param token 令牌 + */ + @SneakyThrows + @GetMapping("/check_token") + public void checkToken(String token, HttpServletResponse response, HttpServletRequest request) { + + ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response); + + if (StrUtil.isBlank(token)) { + httpResponse.setStatusCode(HttpStatus.UNAUTHORIZED); + this.authenticationFailureHandler.onAuthenticationFailure(request, response, + new InvalidBearerTokenException(OAuth2ErrorCodesExpand.TOKEN_MISSING)); + return; + } + OAuth2Authorization authorization = authorizationService.findByToken(token, OAuth2TokenType.ACCESS_TOKEN); + + // 如果令牌不存在 返回401 + if (authorization == null || authorization.getAccessToken() == null) { + this.authenticationFailureHandler.onAuthenticationFailure(request, response, + new InvalidBearerTokenException(OAuth2ErrorCodesExpand.INVALID_BEARER_TOKEN)); + return; + } + + Map claims = authorization.getAccessToken().getClaims(); + OAuth2AccessTokenResponse sendAccessTokenResponse = OAuth2EndpointUtils.sendAccessTokenResponse(authorization, + claims); + this.accessTokenHttpResponseConverter.write(sendAccessTokenResponse, MediaType.APPLICATION_JSON, httpResponse); + } + + /** + * 令牌管理调用 + * + * @param token token + */ + @Inner + @DeleteMapping("/{token}") + public R removeToken(@PathVariable("token") String token) { + OAuth2Authorization authorization = authorizationService.findByToken(token, OAuth2TokenType.ACCESS_TOKEN); + if (authorization == null) { + return R.ok(); + } + + OAuth2Authorization.Token accessToken = authorization.getAccessToken(); + if (accessToken == null || StrUtil.isBlank(accessToken.getToken().getTokenValue())) { + return R.ok(); + } + // 清空用户信息 + cacheManager.getCache(CacheConstants.USER_DETAILS).evict(authorization.getPrincipalName()); + // 清空access token + authorizationService.remove(authorization); + // 处理自定义退出事件,保存相关日志 + SpringContextHolder.publishEvent(new LogoutSuccessEvent(new PreAuthenticatedAuthenticationToken( + authorization.getPrincipalName(), authorization.getRegisteredClientId()))); + return R.ok(); + } + + /** + * 查询token + * + * @param params 分页参数 + * @return + */ + @Inner + @PostMapping("/page") + public R tokenList(@RequestBody Map params) { + // 根据分页参数获取对应数据 + String key = String.format("%s::*", CacheConstants.PROJECT_OAUTH_ACCESS); + int current = MapUtil.getInt(params, CommonConstants.CURRENT); + int size = MapUtil.getInt(params, CommonConstants.SIZE); + Set keys = redisTemplate.keys(key); + List pages = keys.stream().skip((current - 1) * size).limit(size).collect(Collectors.toList()); + Page result = new Page(current, size); + + List tokenVoList = redisTemplate.opsForValue().multiGet(pages).stream().map(obj -> { + OAuth2Authorization authorization = (OAuth2Authorization) obj; + TokenVo tokenVo = new TokenVo(); + tokenVo.setClientId(authorization.getRegisteredClientId()); + tokenVo.setId(authorization.getId()); + tokenVo.setUsername(authorization.getPrincipalName()); + OAuth2Authorization.Token accessToken = authorization.getAccessToken(); + tokenVo.setAccessToken(accessToken.getToken().getTokenValue()); + + String expiresAt = TemporalAccessorUtil.format(accessToken.getToken().getExpiresAt(), + DatePattern.NORM_DATETIME_PATTERN); + tokenVo.setExpiresAt(expiresAt); + + String issuedAt = TemporalAccessorUtil.format(accessToken.getToken().getIssuedAt(), + DatePattern.NORM_DATETIME_PATTERN); + tokenVo.setIssuedAt(issuedAt); + return tokenVo; + }).collect(Collectors.toList()); + result.setRecords(tokenVoList); + result.setTotal(keys.size()); + return R.ok(result); + } + + @GetMapping("sso_login_get_token") + public R> getToken(String ticket, String service) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + PigUser pigUser = (PigUser) authentication.getPrincipal(); + Map ans = new HashMap<>(); + String access_token = tokenManager.createToken(ASSCEE_TOKEN, pigUser.getName(), pigUser.getId().toString()); + String refresh_token = tokenManager.createToken(REFRESH_TOKEN, pigUser.getName(), pigUser.getId().toString()); + redisUtils.setValue(generateTokenKey(ASSCEE_TOKEN, pigUser.getId().toString()), access_token, tokenExpiration); + redisUtils.setValue(generateTokenKey(REFRESH_TOKEN, pigUser.getId().toString()), refresh_token, tokenExpiration); + ans.put("access_token", access_token); + ans.put("refresh_token", refresh_token); + return R.ok(ans); + } + + + private String generateSessionId(String sessionId) { + return String.format(SPRING_SESSION_PREFIX, sessionId); + } + + private String generateTokenKey(String type, String userId) { + return String.format(PIG_TOKEN_PREFIX, type, userId); + } + + +} diff --git a/summer-ospp/2023/pig/pig-auth/src/main/java/com/pig4cloud/pig/auth/utils/RedisUtils.java b/summer-ospp/2023/pig/pig-auth/src/main/java/com/pig4cloud/pig/auth/utils/RedisUtils.java new file mode 100644 index 000000000..ad86cc6e4 --- /dev/null +++ b/summer-ospp/2023/pig/pig-auth/src/main/java/com/pig4cloud/pig/auth/utils/RedisUtils.java @@ -0,0 +1,23 @@ +package com.pig4cloud.pig.auth.utils; + +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +@Component +public class RedisUtils { + + @Resource + private RedisTemplate redisTemplate; + + + public boolean deleteKey(String key) { + return redisTemplate.delete(key); + } + + public void setValue(String key, Object object, Long expire) { + redisTemplate.opsForValue().set(key, object, expire); + } + +} diff --git a/summer-ospp/2023/pig/pig-auth/src/main/java/com/pig4cloud/pig/auth/utils/TokenManager.java b/summer-ospp/2023/pig/pig-auth/src/main/java/com/pig4cloud/pig/auth/utils/TokenManager.java new file mode 100644 index 000000000..6a2c1ada1 --- /dev/null +++ b/summer-ospp/2023/pig/pig-auth/src/main/java/com/pig4cloud/pig/auth/utils/TokenManager.java @@ -0,0 +1,36 @@ +package com.pig4cloud.pig.auth.utils; + +import io.jsonwebtoken.CompressionCodecs; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import org.springframework.stereotype.Component; + +import java.util.Date; + +@Component +public class TokenManager { + private long tokenExpiration = 24 * 60 * 60 * 1000; + private final static String TOKEN_SIGN_KEY = "MAKKEY_PIG"; + + public String createToken(String subject, String username, String id) { + String token = Jwts.builder() + .setSubject(subject) + .claim("nickname", username) + .claim("id", id) + .setExpiration(new Date(System.currentTimeMillis() + tokenExpiration)) + .signWith(SignatureAlgorithm.HS512, TOKEN_SIGN_KEY) + .compressWith(CompressionCodecs.GZIP).compact(); + return token; + } + + + public String getUserFromToken(String token) { + String user = Jwts.parser().setSigningKey(TOKEN_SIGN_KEY).parseClaimsJws(token).getBody().getSubject(); + return user; + } + + public void removeToken(String token) { + //jwttoken无需删除,客户端扔掉即可。 + } + +} diff --git a/summer-ospp/2023/pig/pig-common/pig-common-security/pom.xml b/summer-ospp/2023/pig/pig-common/pig-common-security/pom.xml new file mode 100644 index 000000000..7045c8287 --- /dev/null +++ b/summer-ospp/2023/pig/pig-common/pig-common-security/pom.xml @@ -0,0 +1,81 @@ + + + + + 4.0.0 + + com.pig4cloud + pig-common + 3.6.7 + + + pig-common-security + jar + + pig 安全工具类 + + + + + org.springframework.session + spring-session-data-redis + + + + org.springframework.security + spring-security-cas + + + + com.pig4cloud + pig-common-core + + + cn.hutool + hutool-extra + + + + com.pig4cloud + pig-upms-api + + + + org.springframework.cloud + spring-cloud-commons + + + + org.springframework.cloud + spring-cloud-starter-openfeign + + + org.springframework.security + spring-security-oauth2-jose + + + org.springframework.security + spring-security-oauth2-authorization-server + ${spring.authorization.version} + + + org.springframework + spring-webmvc + + + diff --git a/summer-ospp/2023/pig/pig-common/pig-common-security/src/main/java/com/pig4cloud/pig/common/security/annotation/EnablePigResourceServer.java b/summer-ospp/2023/pig/pig-common/pig-common-security/src/main/java/com/pig4cloud/pig/common/security/annotation/EnablePigResourceServer.java new file mode 100644 index 000000000..7ee86239a --- /dev/null +++ b/summer-ospp/2023/pig/pig-common/pig-common-security/src/main/java/com/pig4cloud/pig/common/security/annotation/EnablePigResourceServer.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2020 pig4cloud Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.pig4cloud.pig.common.security.annotation; + +import com.pig4cloud.pig.common.security.component.PigResourceServerAutoConfiguration; +import com.pig4cloud.pig.common.security.component.PigResourceServerConfiguration; +import com.pig4cloud.pig.common.security.component.ResourceAuthExceptionEntryPoint; +import com.pig4cloud.pig.common.security.config.*; +import org.springframework.context.annotation.Import; + +import java.lang.annotation.*; + +/** + * @author lengleng + * @date 2022-06-04 + *

+ * 资源服务注解 + */ +@Documented +@Inherited +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +//@Import({ PigResourceServerAutoConfiguration.class, PigResourceServerConfiguration.class }) +@Import({PigResourceServerAutoConfiguration.class, CasProperties.class, SecurityConfig.class}) +public @interface EnablePigResourceServer { + +} diff --git a/summer-ospp/2023/pig/pig-common/pig-common-security/src/main/java/com/pig4cloud/pig/common/security/config/CasProperties.java b/summer-ospp/2023/pig/pig-common/pig-common-security/src/main/java/com/pig4cloud/pig/common/security/config/CasProperties.java new file mode 100644 index 000000000..1eda0a6f0 --- /dev/null +++ b/summer-ospp/2023/pig/pig-common/pig-common-security/src/main/java/com/pig4cloud/pig/common/security/config/CasProperties.java @@ -0,0 +1,59 @@ +package com.pig4cloud.pig.common.security.config; + +import lombok.Data; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Data +@Component +public class CasProperties { + + /** + * 秘钥 + */ + @Value("${cas.key}") + private String casKey; + + /** + * cas服务端地址 + */ + @Value("${cas.server.host.url}") + private String casServerUrl; + + /** + * cas服务端地址 + */ + @Value("${cas.server.host.grant_url}") + private String casGrantingUrl; + + /** + * cas服务端登录地址 + */ + @Value("${cas.server.host.login_url}") + private String casServerLoginUrl; + + /** + * cas服务端登出地址 并回跳到制定页面 + */ + @Value("${cas.server.host.logout_url}") + private String casServerLogoutUrl; + + /** + * cas客户端地址 + */ + @Value("${cas.service.host.url}") + private String casServiceUrl; + + /** + * cas客户端地址登录地址 + */ + @Value("${cas.service.host.login_url}") + private String casServiceLoginUrl; + + /** + * cas客户端地址登出地址 + */ + @Value("${cas.service.host.logout_url}") + private String casServiceLogoutUrl; + +} diff --git a/summer-ospp/2023/pig/pig-common/pig-common-security/src/main/java/com/pig4cloud/pig/common/security/config/SecurityConfig.java b/summer-ospp/2023/pig/pig-common/pig-common-security/src/main/java/com/pig4cloud/pig/common/security/config/SecurityConfig.java new file mode 100644 index 000000000..8ed246682 --- /dev/null +++ b/summer-ospp/2023/pig/pig-common/pig-common-security/src/main/java/com/pig4cloud/pig/common/security/config/SecurityConfig.java @@ -0,0 +1,156 @@ +package com.pig4cloud.pig.common.security.config; + +import cn.hutool.core.util.ArrayUtil; +import com.pig4cloud.pig.common.security.component.PermitAllUrlProperties; +import com.pig4cloud.pig.common.security.component.PigBearerTokenExtractor; +import com.pig4cloud.pig.common.security.component.ResourceAuthExceptionEntryPoint; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.jasig.cas.client.session.SingleSignOutFilter; +import org.jasig.cas.client.validation.Cas20ServiceTicketValidator; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.cas.ServiceProperties; +import org.springframework.security.cas.authentication.CasAuthenticationProvider; +import org.springframework.security.cas.web.CasAuthenticationEntryPoint; +import org.springframework.security.cas.web.CasAuthenticationFilter; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.core.userdetails.AuthenticationUserDetailsService; +import org.springframework.security.web.authentication.logout.LogoutFilter; +import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler; + +@Slf4j +@Configuration +@EnableWebSecurity // 启用web权限 +@EnableGlobalMethodSecurity(prePostEnabled = true) // 启用方法验证 +@RequiredArgsConstructor +public class SecurityConfig extends WebSecurityConfigurerAdapter { + + @Autowired + private CasProperties casProperties; + + @Autowired + private AuthenticationUserDetailsService casUserDetailService; + + private final PermitAllUrlProperties permitAllUrl; + + + /** + * 定义认证用户信息获取来源,密码校验规则等 + */ + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + super.configure(auth); + auth.authenticationProvider(casAuthenticationProvider()); + } + + /** + * 定义安全策略 + */ + @Override + protected void configure(HttpSecurity http) throws Exception { + + http.authorizeRequests()// 配置安全策略 + .antMatchers(ArrayUtil.toArray(permitAllUrl.getUrls(), String.class)).permitAll() + .anyRequest().authenticated()// 其余的所有请求都需要验证 + .and().logout().permitAll()// 定义logout不需要验证 + .and().formLogin();// 使用form表单登录 + + http.exceptionHandling() + .authenticationEntryPoint(casAuthenticationEntryPoint()) + .and() + .addFilter(casAuthenticationFilter()) + .addFilterBefore(casLogoutFilter(), LogoutFilter.class) + .addFilterBefore(singleSignOutFilter(), CasAuthenticationFilter.class); + // 取消跨站请求伪造防护 + http.csrf().disable(); +// // 防止iframe 造成跨域 + http.headers().frameOptions().disable(); + // http.csrf().disable(); //禁用CSRF + } + + /** + * 认证的入口 + */ + @Bean + public CasAuthenticationEntryPoint casAuthenticationEntryPoint() { + CasAuthenticationEntryPoint casAuthenticationEntryPoint = new CasAuthenticationEntryPoint(); + casAuthenticationEntryPoint.setLoginUrl(casProperties.getCasServerLoginUrl()); + casAuthenticationEntryPoint.setServiceProperties(serviceProperties()); + return casAuthenticationEntryPoint; + } + + /** + * 指定service相关信息 + */ + @Bean + public ServiceProperties serviceProperties() { + ServiceProperties serviceProperties = new ServiceProperties(); + //设置cas客户端登录完整的url + serviceProperties.setService(casProperties.getCasServiceUrl() + casProperties.getCasServiceLoginUrl()); + serviceProperties.setSendRenew(false); + serviceProperties.setAuthenticateAllArtifacts(true); + return serviceProperties; + } + + /** + * CAS认证过滤器 + */ + @Bean + public CasAuthenticationFilter casAuthenticationFilter() throws Exception { + CasAuthenticationFilter casAuthenticationFilter = new CasAuthenticationFilter(); + casAuthenticationFilter.setAuthenticationManager(authenticationManager()); + casAuthenticationFilter.setFilterProcessesUrl(casProperties.getCasServiceUrl() + casProperties.getCasServiceLoginUrl()); + casAuthenticationFilter.setServiceProperties(serviceProperties()); + return casAuthenticationFilter; + } + + /** + * cas 认证 Provider + */ + @Bean + public CasAuthenticationProvider casAuthenticationProvider() { + CasAuthenticationProvider casAuthenticationProvider = new CasAuthenticationProvider(); + casAuthenticationProvider.setAuthenticationUserDetailsService(casUserDetailService); + // //这里只是接口类型,实现的接口不一样,都可以的。 + casAuthenticationProvider.setServiceProperties(serviceProperties()); + casAuthenticationProvider.setTicketValidator(cas20ServiceTicketValidator()); + casAuthenticationProvider.setKey("casAuthenticationProviderKey"); + return casAuthenticationProvider; + } + + + + + @Bean + public Cas20ServiceTicketValidator cas20ServiceTicketValidator() { + return new Cas20ServiceTicketValidator(casProperties.getCasGrantingUrl()); + } + + /** + * 单点登出过滤器 + */ + @Bean + public SingleSignOutFilter singleSignOutFilter() { + SingleSignOutFilter singleSignOutFilter = new SingleSignOutFilter(); + singleSignOutFilter.setLogoutCallbackPath(casProperties.getCasServerUrl()); + singleSignOutFilter.setIgnoreInitConfiguration(true); + return singleSignOutFilter; + } + + /** + * 请求单点退出过滤器 + */ + @Bean + public LogoutFilter casLogoutFilter() { + LogoutFilter logoutFilter = new LogoutFilter(casProperties.getCasServerLogoutUrl(), new SecurityContextLogoutHandler()); + logoutFilter.setFilterProcessesUrl(casProperties.getCasServiceLogoutUrl()); + return logoutFilter; + } +} diff --git a/summer-ospp/2023/pig/pig-common/pig-common-security/src/main/java/com/pig4cloud/pig/common/security/service/PigUserDetailsServiceImpl.java b/summer-ospp/2023/pig/pig-common/pig-common-security/src/main/java/com/pig4cloud/pig/common/security/service/PigUserDetailsServiceImpl.java new file mode 100644 index 000000000..35f8b1b40 --- /dev/null +++ b/summer-ospp/2023/pig/pig-common/pig-common-security/src/main/java/com/pig4cloud/pig/common/security/service/PigUserDetailsServiceImpl.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2020 pig4cloud Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.pig4cloud.pig.common.security.service; + +import com.pig4cloud.pig.admin.api.dto.UserInfo; +import com.pig4cloud.pig.admin.api.feign.RemoteUserService; +import com.pig4cloud.pig.common.core.constant.CacheConstants; +import com.pig4cloud.pig.common.core.util.R; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.context.annotation.Primary; +import org.springframework.security.cas.authentication.CasAssertionAuthenticationToken; +import org.springframework.security.core.userdetails.AuthenticationUserDetailsService; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UsernameNotFoundException; + +import java.util.*; + +/** + * 用户详细信息 + * + * @author lengleng hccake + */ +@Slf4j +@Primary +@RequiredArgsConstructor +public class PigUserDetailsServiceImpl implements PigUserDetailsService, AuthenticationUserDetailsService { + + private final RemoteUserService remoteUserService; + + private final CacheManager cacheManager; + + /** + * 用户名密码登录 + * + * @param username 用户名 + * @return + */ + @Override + @SneakyThrows + public UserDetails loadUserByUsername(String username) { + Cache cache = cacheManager.getCache(CacheConstants.USER_DETAILS); + if (cache != null && cache.get(username) != null) { + return (PigUser) cache.get(username).get(); + } + return getUserDetails(remoteUserService.info(username), username); + } + + @Override + public int getOrder() { + return Integer.MIN_VALUE; + } + + @Override + public UserDetails loadUserDetails(CasAssertionAuthenticationToken token) throws UsernameNotFoundException { + log.info("getCredentials:{}", token.getCredentials()); + String username = token.getName(); + Cache cache = cacheManager.getCache(CacheConstants.USER_DETAILS); + if (cache != null && cache.get(username) != null) { + return (PigUser) cache.get(username).get(); + } + R result = remoteUserService.saveIfNotExist(token.getAssertion().getPrincipal().getAttributes()); + return getUserDetails(result, username); + } + + private UserDetails getUserDetails(R result, String username) { + Cache cache = cacheManager.getCache(CacheConstants.USER_DETAILS); + UserDetails userDetails = getUserDetails(result); + if (cache != null) { + cache.put(username, userDetails); + } + return userDetails; + } + + +} diff --git a/summer-ospp/2023/pig/pig-common/pig-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/summer-ospp/2023/pig/pig-common/pig-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 000000000..41365bcb5 --- /dev/null +++ b/summer-ospp/2023/pig/pig-common/pig-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,8 @@ +com.pig4cloud.pig.common.security.service.PigUserDetailsServiceImpl +com.pig4cloud.pig.common.security.service.PigAppUserDetailsServiceImpl +com.pig4cloud.pig.common.security.service.PigRedisOAuth2AuthorizationService +com.pig4cloud.pig.common.security.service.PigRedisOAuth2AuthorizationConsentService +com.pig4cloud.pig.common.security.component.PigSecurityInnerAspect +com.pig4cloud.pig.common.security.component.PigSecurityMessageSourceConfiguration +com.pig4cloud.pig.common.security.service.PigRemoteRegisteredClientRepository + diff --git a/summer-ospp/2023/pig/pig-common/pig-common-security/src/main/resources/i18n/errors/messages_zh_CN.properties b/summer-ospp/2023/pig/pig-common/pig-common-security/src/main/resources/i18n/errors/messages_zh_CN.properties new file mode 100644 index 000000000..86668a5d8 --- /dev/null +++ b/summer-ospp/2023/pig/pig-common/pig-common-security/src/main/resources/i18n/errors/messages_zh_CN.properties @@ -0,0 +1,50 @@ +AbstractAccessDecisionManager.accessDenied=\u4E0D\u5141\u8BB8\u8BBF\u95EE +AbstractLdapAuthenticationProvider.emptyPassword=\u7528\u6237\u540D\u6216\u5BC6\u7801\u9519\u8BEF +AbstractSecurityInterceptor.authenticationNotFound=\u672A\u5728SecurityContext\u4E2D\u67E5\u627E\u5230\u8BA4\u8BC1\u5BF9\u8C61 +OAuth2ResourceOwnerBaseAuthenticationProvider.tokenExpired=\u8BF7\u6C42\u4EE4\u724C\u5DF2\u8FC7\u671F +AbstractUserDetailsAuthenticationProvider.smsBadCredentials=\u624B\u673A\u53F7\u4E0D\u5B58\u5728\u767B\u5F55\u5931\u8D25 +AbstractUserDetailsAuthenticationProvider.badCredentials=\u7528\u6237\u540D\u6216\u5BC6\u7801\u9519\u8BEF +AbstractUserDetailsAuthenticationProvider.credentialsExpired=\u7528\u6237\u51ED\u8BC1\u5DF2\u8FC7\u671F +AbstractUserDetailsAuthenticationProvider.disabled=\u7528\u6237\u5DF2\u5931\u6548 +AbstractUserDetailsAuthenticationProvider.expired=\u7528\u6237\u5E10\u53F7\u5DF2\u8FC7\u671F +AbstractUserDetailsAuthenticationProvider.locked=\u7528\u6237\u5E10\u53F7\u5DF2\u88AB\u9501\u5B9A +AbstractUserDetailsAuthenticationProvider.onlySupports=\u4EC5\u4EC5\u652F\u6301UsernamePasswordAuthenticationToken +AccountStatusUserDetailsChecker.credentialsExpired=\u7528\u6237\u51ED\u8BC1\u5DF2\u8FC7\u671F +AccountStatusUserDetailsChecker.disabled=\u7528\u6237\u5DF2\u5931\u6548 +AccountStatusUserDetailsChecker.expired=\u7528\u6237\u5E10\u53F7\u5DF2\u8FC7\u671F +AccountStatusUserDetailsChecker.locked=\u7528\u6237\u5E10\u53F7\u5DF2\u88AB\u9501\u5B9A +AclEntryAfterInvocationProvider.noPermission=\u7ED9\u5B9A\u7684Authentication\u5BF9\u8C61({0})\u6839\u672C\u65E0\u6743\u64CD\u63A7\u9886\u57DF\u5BF9\u8C61({1}) +AnonymousAuthenticationProvider.incorrectKey=\u5C55\u793A\u7684AnonymousAuthenticationToken\u4E0D\u542B\u6709\u9884\u671F\u7684key +BindAuthenticator.badCredentials=\u7528\u6237\u540D\u6216\u5BC6\u7801\u9519\u8BEF +BindAuthenticator.emptyPassword=\u7528\u6237\u540D\u6216\u5BC6\u7801\u9519\u8BEF +CasAuthenticationProvider.incorrectKey=\u5C55\u793A\u7684CasAuthenticationToken\u4E0D\u542B\u6709\u9884\u671F\u7684key +CasAuthenticationProvider.noServiceTicket=\u672A\u80FD\u591F\u6B63\u786E\u63D0\u4F9B\u5F85\u9A8C\u8BC1\u7684CAS\u670D\u52A1\u7968\u6839 +ConcurrentSessionControlAuthenticationStrategy.exceededAllowed=\u5DF2\u7ECF\u8D85\u8FC7\u4E86\u5F53\u524D\u4E3B\u4F53({0})\u88AB\u5141\u8BB8\u7684\u6700\u5927\u4F1A\u8BDD\u6570\u91CF +DigestAuthenticationFilter.incorrectRealm=\u54CD\u5E94\u7ED3\u679C\u4E2D\u7684Realm\u540D\u5B57({0})\u540C\u7CFB\u7EDF\u6307\u5B9A\u7684Realm\u540D\u5B57({1})\u4E0D\u543B\u5408 +DigestAuthenticationFilter.incorrectResponse=\u9519\u8BEF\u7684\u54CD\u5E94\u7ED3\u679C +DigestAuthenticationFilter.missingAuth=\u9057\u6F0F\u4E86\u9488\u5BF9'auth' QOP\u7684\u3001\u5FC5\u987B\u7ED9\u5B9A\u7684\u6458\u8981\u53D6\u503C; \u63A5\u6536\u5230\u7684\u5934\u4FE1\u606F\u4E3A{0} +DigestAuthenticationFilter.missingMandatory=\u9057\u6F0F\u4E86\u5FC5\u987B\u7ED9\u5B9A\u7684\u6458\u8981\u53D6\u503C; \u63A5\u6536\u5230\u7684\u5934\u4FE1\u606F\u4E3A{0} +DigestAuthenticationFilter.nonceCompromised=Nonce\u4EE4\u724C\u5DF2\u7ECF\u5B58\u5728\u95EE\u9898\u4E86\uFF0C{0} +DigestAuthenticationFilter.nonceEncoding=Nonce\u672A\u7ECF\u8FC7Base64\u7F16\u7801; \u76F8\u5E94\u7684nonce\u53D6\u503C\u4E3A {0} +DigestAuthenticationFilter.nonceExpired=Nonce\u5DF2\u7ECF\u8FC7\u671F/\u8D85\u65F6 +DigestAuthenticationFilter.nonceNotNumeric=Nonce\u4EE4\u724C\u7684\u7B2C1\u90E8\u5206\u5E94\u8BE5\u662F\u6570\u5B57\uFF0C\u4F46\u7ED3\u679C\u5374\u662F{0} +DigestAuthenticationFilter.nonceNotTwoTokens=Nonce\u5E94\u8BE5\u7531\u4E24\u90E8\u5206\u53D6\u503C\u6784\u6210\uFF0C\u4F46\u7ED3\u679C\u5374\u662F{0} +DigestAuthenticationFilter.usernameNotFound=\u7528\u6237\u540D{0}\u672A\u627E\u5230 +ExceptionTranslationFilter.insufficientAuthentication=\u8BBF\u95EE\u6B64\u8D44\u6E90\u9700\u8981\u5B8C\u5168\u8EAB\u4EFD\u9A8C\u8BC1 +JdbcDaoImpl.noAuthority=\u6CA1\u6709\u4E3A\u7528\u6237{0}\u6307\u5B9A\u89D2\u8272 +JdbcDaoImpl.notFound=\u672A\u627E\u5230\u7528\u6237{0} +LdapAuthenticationProvider.badCredentials=\u7528\u6237\u540D\u6216\u5BC6\u7801\u9519\u8BEF +LdapAuthenticationProvider.credentialsExpired=\u7528\u6237\u51ED\u8BC1\u5DF2\u8FC7\u671F +LdapAuthenticationProvider.disabled=\u7528\u6237\u5DF2\u5931\u6548 +LdapAuthenticationProvider.expired=\u7528\u6237\u5E10\u53F7\u5DF2\u8FC7\u671F +LdapAuthenticationProvider.locked=\u7528\u6237\u5E10\u53F7\u5DF2\u88AB\u9501\u5B9A +LdapAuthenticationProvider.emptyUsername=\u7528\u6237\u540D\u4E0D\u5141\u8BB8\u4E3A\u7A7A +LdapAuthenticationProvider.onlySupports=\u4EC5\u4EC5\u652F\u6301UsernamePasswordAuthenticationToken +PasswordComparisonAuthenticator.badCredentials=\u7528\u6237\u540D\u6216\u5BC6\u7801\u9519\u8BEF +#PersistentTokenBasedRememberMeServices.cookieStolen=Invalid remember-me token (Series/token) mismatch. Implies previous cookie theft attack. +ProviderManager.providerNotFound=\u672A\u67E5\u627E\u5230\u9488\u5BF9{0}\u7684AuthenticationProvider +RememberMeAuthenticationProvider.incorrectKey=\u5C55\u793ARememberMeAuthenticationToken\u4E0D\u542B\u6709\u9884\u671F\u7684key +RunAsImplAuthenticationProvider.incorrectKey=\u5C55\u793A\u7684RunAsUserToken\u4E0D\u542B\u6709\u9884\u671F\u7684key +SubjectDnX509PrincipalExtractor.noMatching=\u672A\u5728subjectDN\: {0}\u4E2D\u627E\u5230\u5339\u914D\u7684\u6A21\u5F0F +SwitchUserFilter.noCurrentUser=\u4E0D\u5B58\u5728\u5F53\u524D\u7528\u6237 +SwitchUserFilter.noOriginalAuthentication=\u4E0D\u80FD\u591F\u67E5\u627E\u5230\u539F\u5148\u7684\u5DF2\u8BA4\u8BC1\u5BF9\u8C61 diff --git a/summer-ospp/2023/pig/pig-common/pom.xml b/summer-ospp/2023/pig/pig-common/pom.xml new file mode 100644 index 000000000..15908dee5 --- /dev/null +++ b/summer-ospp/2023/pig/pig-common/pom.xml @@ -0,0 +1,45 @@ + + + + + 4.0.0 + + com.pig4cloud + pig + 3.6.7 + + + pig-common + pom + + pig 公共聚合模块 + + + pig-common-bom + pig-common-core + pig-common-datasource + pig-common-job + pig-common-log + pig-common-mybatis + pig-common-seata + pig-common-security + pig-common-feign + pig-common-swagger + pig-common-xss + + diff --git a/summer-ospp/2023/pig/pig-upms/pig-upms-biz/pom.xml b/summer-ospp/2023/pig/pig-upms/pig-upms-biz/pom.xml new file mode 100644 index 000000000..3a5deef56 --- /dev/null +++ b/summer-ospp/2023/pig/pig-upms/pig-upms-biz/pom.xml @@ -0,0 +1,135 @@ + + + + + 4.0.0 + + com.pig4cloud + pig-upms + 3.6.7 + + + pig-upms-biz + jar + + pig 通用用户权限管理系统业务处理模块 + + + + com.google.guava + guava + 29.0-jre + + + + com.pig4cloud + pig-upms-api + + + + com.pig4cloud.plugin + oss-spring-boot-starter + + + + com.pig4cloud + pig-common-feign + + + + com.pig4cloud + pig-common-security + + + + com.pig4cloud + pig-common-log + + + + com.pig4cloud + pig-common-swagger + + + + com.baomidou + mybatis-plus-boot-starter + + + com.mysql + mysql-connector-j + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-config + + + + io.springboot.sms + aliyun-sms-spring-boot-starter + + + + com.pig4cloud + pig-common-xss + + + + org.springframework.boot + spring-boot-starter-undertow + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + io.fabric8 + docker-maven-plugin + + + + + src/main/resources + true + + **/*.xlsx + **/*.xls + + + + src/main/resources + false + + **/*.xlsx + **/*.xls + + + + + + diff --git a/summer-ospp/2023/pig/pig-upms/pig-upms-biz/src/main/java/com/pig4cloud/pig/admin/controller/SysUserController.java b/summer-ospp/2023/pig/pig-upms/pig-upms-biz/src/main/java/com/pig4cloud/pig/admin/controller/SysUserController.java new file mode 100644 index 000000000..670f47896 --- /dev/null +++ b/summer-ospp/2023/pig/pig-upms/pig-upms-biz/src/main/java/com/pig4cloud/pig/admin/controller/SysUserController.java @@ -0,0 +1,269 @@ +/* + * Copyright (c) 2020 pig4cloud Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.pig4cloud.pig.admin.controller; + +import cn.hutool.core.collection.CollUtil; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.google.common.collect.Lists; +import com.pig4cloud.pig.admin.api.dto.UserDTO; +import com.pig4cloud.pig.admin.api.dto.UserInfo; +import com.pig4cloud.pig.admin.api.entity.SysUser; +import com.pig4cloud.pig.admin.api.vo.UserExcelVO; +import com.pig4cloud.pig.admin.api.vo.UserInfoVO; +import com.pig4cloud.pig.admin.api.vo.UserVO; +import com.pig4cloud.pig.admin.service.SysUserService; +import com.pig4cloud.pig.admin.utils.BeanCreator; +import com.pig4cloud.pig.common.core.exception.ErrorCodes; +import com.pig4cloud.pig.common.core.util.MsgUtils; +import com.pig4cloud.pig.common.core.util.R; +import com.pig4cloud.pig.common.log.annotation.SysLog; +import com.pig4cloud.pig.common.security.annotation.Inner; +import com.pig4cloud.pig.common.security.util.SecurityUtils; +import com.pig4cloud.pig.common.xss.core.XssCleanIgnore; +import com.pig4cloud.plugin.excel.annotation.RequestExcel; +import com.pig4cloud.plugin.excel.annotation.ResponseExcel; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import org.springframework.beans.BeanUtils; +import org.springframework.http.HttpHeaders; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * @author lengleng + * @date 2019/2/1 + */ +@Slf4j +@RestController +@RequiredArgsConstructor +@RequestMapping("/user") +@Tag(name = "用户管理模块") +@SecurityRequirement(name = HttpHeaders.AUTHORIZATION) +public class SysUserController { + + private final SysUserService userService; + + /** + * 获取当前用户全部信息 + * + * @return 用户信息 + */ + @GetMapping(value = {"/info"}) + public R info() { + String username = SecurityUtils.getUser().getUsername(); + SysUser user = userService.getOne(Wrappers.query().lambda().eq(SysUser::getUsername, username)); + if (user == null) { + return R.failed(MsgUtils.getMessage(ErrorCodes.SYS_USER_QUERY_ERROR)); + } + UserInfo userInfo = userService.getUserInfo(user); + UserInfoVO vo = new UserInfoVO(); + vo.setSysUser(userInfo.getSysUser()); + vo.setRoles(userInfo.getRoles()); + vo.setPermissions(userInfo.getPermissions()); + return R.ok(vo); + } + + /** + * 获取指定用户全部信息 + * + * @return 用户信息 + */ + @Inner + @GetMapping("/info/{username}") + public R info(@PathVariable String username) { + SysUser user = userService.getOne(Wrappers.query().lambda().eq(SysUser::getUsername, username)); + if (user == null) { + return R.failed(MsgUtils.getMessage(ErrorCodes.SYS_USER_USERINFO_EMPTY, username)); + } + return R.ok(userService.getUserInfo(user)); + } + + @Inner + @PostMapping("/sso_save") + public R save_sso(@RequestBody Map attributes) { + String username = (String) attributes.getOrDefault("username", "pig"); + // 判断用户名是否存在 + SysUser sysUser = userService.getOne(Wrappers.lambdaQuery().eq(SysUser::getUsername, username)); + if (sysUser == null) { + SysUser sysUserByMap = BeanCreator.createSysUserByMap(attributes); + UserDTO userDTO = new UserDTO(); + BeanUtils.copyProperties(sysUserByMap,userDTO); + + userDTO.setPost(Lists.newArrayList(1l)); + userDTO.setDeptId(6l); + userDTO.setRole(Lists.newArrayList(2l)); + // 添加用户 + userService.saveUser(userDTO); + } + return info(username); + } + + /** + * 根据部门id,查询对应的用户 id 集合 + * + * @param deptIds 部门id 集合 + * @return 用户 id 集合 + */ + @Inner + @GetMapping("/ids") + public R> listUserIdByDeptIds(@RequestParam("deptIds") Set deptIds) { + return R.ok(userService.listUserIdByDeptIds(deptIds)); + } + + /** + * 通过ID查询用户信息 + * + * @param id ID + * @return 用户信息 + */ + @GetMapping("/{id:\\d+}") + public R user(@PathVariable Long id) { + return R.ok(userService.getUserVoById(id)); + } + + /** + * 判断用户是否存在 + * + * @param userDTO 查询条件 + * @return + */ + @Inner(false) + @GetMapping("/check/exist") + public R isExist(UserDTO userDTO) { + List sysUserList = userService.list(new QueryWrapper<>(userDTO)); + if (CollUtil.isNotEmpty(sysUserList)) { + return R.ok(Boolean.TRUE, MsgUtils.getMessage(ErrorCodes.SYS_USER_EXISTING)); + } + return R.ok(Boolean.FALSE); + } + + /** + * 删除用户信息 + * + * @param id ID + * @return R + */ + @SysLog("删除用户信息") + @DeleteMapping("/{id:\\d+}") + @PreAuthorize("@pms.hasPermission('sys_user_del')") + public R userDel(@PathVariable Long id) { + SysUser sysUser = userService.getById(id); + return R.ok(userService.removeUserById(sysUser)); + } + + /** + * 添加用户 + * + * @param userDto 用户信息 + * @return success/false + */ + @SysLog("添加用户") + @PostMapping + @XssCleanIgnore({"password"}) + @PreAuthorize("@pms.hasPermission('sys_user_add')") + public R user(@RequestBody UserDTO userDto) { + return R.ok(userService.saveUser(userDto)); + } + + /** + * 管理员更新用户信息 + * + * @param userDto 用户信息 + * @return R + */ + @SysLog("更新用户信息") + @PutMapping + @XssCleanIgnore({"password"}) + @PreAuthorize("@pms.hasPermission('sys_user_edit')") + public R updateUser(@Valid @RequestBody UserDTO userDto) { + return userService.updateUser(userDto); + } + + /** + * 分页查询用户 + * + * @param page 参数集 + * @param userDTO 查询参数列表 + * @return 用户集合 + */ + @GetMapping("/page") + public R> getUserPage(Page page, UserDTO userDTO) { + return R.ok(userService.getUserWithRolePage(page, userDTO)); + } + + /** + * 个人修改个人信息 + * + * @param userDto userDto + * @return success/false + */ + @SysLog("修改个人信息") + @PutMapping("/edit") + @XssCleanIgnore({"password", "newpassword1"}) + public R updateUserInfo(@Valid @RequestBody UserDTO userDto) { + userDto.setUsername(SecurityUtils.getUser().getUsername()); + return userService.updateUserInfo(userDto); + } + + /** + * @param username 用户名称 + * @return 上级部门用户列表 + */ + @GetMapping("/ancestor/{username}") + public R> listAncestorUsers(@PathVariable String username) { + return R.ok(userService.listAncestorUsersByUsername(username)); + } + + /** + * 导出excel 表格 + * + * @param userDTO 查询条件 + * @return + */ + @ResponseExcel + @GetMapping("/export") + @PreAuthorize("@pms.hasPermission('sys_user_import_export')") + public List export(UserDTO userDTO) { + return userService.listUser(userDTO); + } + + /** + * 导入用户 + * + * @param excelVOList 用户列表 + * @param bindingResult 错误信息列表 + * @return R + */ + @PostMapping("/import") + @PreAuthorize("@pms.hasPermission('sys_user_import_export')") + public R importUser(@RequestExcel List excelVOList, BindingResult bindingResult) { + return userService.importUser(excelVOList, bindingResult); + } + +} diff --git a/summer-ospp/2023/pig/pig-upms/pig-upms-biz/src/main/java/com/pig4cloud/pig/admin/utils/BeanCreator.java b/summer-ospp/2023/pig/pig-upms/pig-upms-biz/src/main/java/com/pig4cloud/pig/admin/utils/BeanCreator.java new file mode 100644 index 000000000..e0ea3dca6 --- /dev/null +++ b/summer-ospp/2023/pig/pig-upms/pig-upms-biz/src/main/java/com/pig4cloud/pig/admin/utils/BeanCreator.java @@ -0,0 +1,23 @@ +package com.pig4cloud.pig.admin.utils; + +import com.pig4cloud.pig.admin.api.entity.SysUser; + +import java.util.Map; + +public class BeanCreator { + + private static final String DEFAULT_PASSWORD = "pigmax123456"; + + public static SysUser createSysUserByMap(Map map) { + SysUser sysUser = new SysUser(); + String username = (String) map.get("username"); + String phone = (String) map.get("mobile"); + String deptId = (String) map.get("departmentId"); + sysUser.setUsername(username); + sysUser.setPhone(phone); + sysUser.setDeptId(Long.parseLong(deptId)); + sysUser.setPassword(DEFAULT_PASSWORD); + return sysUser; + } + +} diff --git a/summer-ospp/2023/pig/vue/src/api/login.js b/summer-ospp/2023/pig/vue/src/api/login.js new file mode 100644 index 000000000..62f9b69d4 --- /dev/null +++ b/summer-ospp/2023/pig/vue/src/api/login.js @@ -0,0 +1,179 @@ +/* + * 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) + */ +import { validatenull } from '@/util/validate' +import request from '@/router/axios' +import store from '@/store' +import qs from 'qs' +import { getStore, setStore } from '@/util/store.js' +import website from '@/config/website' + + +const scope = 'server' + +export const loginByUsername = (username, password, code, randomStr) => { + const grant_type = 'password' + const dataObj = qs.stringify({ 'username': username, 'password': password }) + + const basicAuth = 'Basic ' + window.btoa(website.formLoginClient) + + // 保存当前选中的 basic 认证信息 + setStore({ + name: 'basicAuth', + content: basicAuth, + type: 'session' + }) + + return request({ + url: '/auth/oauth2/token', + headers: { + isToken: false, + Authorization: basicAuth + }, + method: 'post', + params: { randomStr, code, grant_type, scope }, + data: dataObj + }) +} + +export const loginByMobile = (smsForm) => { + const grant_type = 'app' + + const basicAuth = 'Basic ' + window.btoa(website.smsLoginClient) + + // 保存当前选中的 basic 认证信息 + setStore({ + name: 'basicAuth', + content: basicAuth, + type: 'session' + }) + + return request({ + url: '/auth/oauth2/token', + headers: { + isToken: false, + 'Authorization': basicAuth + }, + method: 'post', + params: { phone: smsForm.phone, code: smsForm.code, grant_type, scope } + }) +} + +export const ssoLogin = (ticket,service) => { + return request({ + url: '/auth/token/sso_login_get_token', + method: 'get', + params: { ticket, service } + }) +} + +export const refreshToken = refresh_token => { + const grant_type = 'refresh_token' + // 获取当前选中的 basic 认证信息 + const basicAuth = getStore({ name: 'basicAuth' }) + + return request({ + url: '/auth/oauth2/token', + headers: { + isToken: false, + Authorization: basicAuth + }, + method: 'post', + params: { refresh_token, grant_type, scope } + }) +} + +export const getUserInfo = () => { + return request({ + url: '/admin/user/info', + method: 'get' + }) +} + +export const logout = () => { + return request({ + url: '/auth/token/logout', + method: 'delete' + }) +} + +/** + * 校验令牌,若有效期小于半小时自动续期 + * + * 定时任务请求后端接口返回实际的有效时间,不进行本地计算避免 客户端和服务器机器时钟不一致 + * @param refreshLock + */ +export const checkToken = (refreshLock, $store) => { + const token = store.getters.access_token + // 获取当前选中的 basic 认证信息 + const basicAuth = getStore({ name: 'basicAuth' }) + + if (validatenull(token) || validatenull(basicAuth)) { + return + } + + request({ + url: '/auth/token/check_token', + headers: { + isToken: false, + Authorization: basicAuth + }, + method: 'get', + params: { token } + }).then(response => { + const expire = response && response.data && response.data.exp + if (expire) { + const expiredPeriod = expire * 1000 - new Date().getTime() + console.log('当前token过期时间', expiredPeriod, '毫秒') + //小于半小时自动续约 + if (expiredPeriod <= website.remainingTime) { + if (!refreshLock) { + refreshLock = true + $store.dispatch('RefreshToken') + .catch(() => { + clearInterval(this.refreshTime) + }) + refreshLock = false + } + } + } + }).catch(error => { + console.error(error) + }) +} + +/** + * 注册用户 + */ +export const registerUser = (userInfo) => { + return request({ + url: '/admin/register/user', + method: 'post', + data: userInfo + }) +} + + +/** + * 发送短信 + */ +export const sendSmsCode = (form) => { + return request({ + url: '/admin/app/sms', + method: 'post', + data: form + }) +} diff --git a/summer-ospp/2023/pig/vue/src/page/login/userlogin.vue b/summer-ospp/2023/pig/vue/src/page/login/userlogin.vue new file mode 100644 index 000000000..09036637d --- /dev/null +++ b/summer-ospp/2023/pig/vue/src/page/login/userlogin.vue @@ -0,0 +1,169 @@ + + + + + diff --git a/summer-ospp/2023/pig/vue/src/router/axios.js b/summer-ospp/2023/pig/vue/src/router/axios.js new file mode 100644 index 000000000..5ea468c8c --- /dev/null +++ b/summer-ospp/2023/pig/vue/src/router/axios.js @@ -0,0 +1,99 @@ + +import axios from 'axios' +import { serialize } from '@/util' +import NProgress from 'nprogress' // progress bar +import errorCode from '@/const/errorCode' +import { ElMessage, ElMessageBox } from 'element-plus' +import 'nprogress/nprogress.css' +import qs from 'qs' +import store from '@/store' +import router from '@/router/index.js' +import { baseUrl } from '@/config/env' // progress bar style +axios.defaults.timeout = 30000 +// 返回其他状态吗 +axios.defaults.validateStatus = function(status) { + return status >= 200 && status <= 500 // 默认的 +} +// 跨域请求,允许保存cookie +axios.defaults.withCredentials = true +// NProgress Configuration +NProgress.configure({ + showSpinner: false +}) + +// HTTPrequest拦截 +axios.defaults.baseURL = baseUrl +axios.interceptors.request.use(config => { + NProgress.start() // start progress bar + const isToken = (config.headers || {}).isToken === false + const token = store.getters.access_token + + if (token && !isToken) { + config.headers['Authorization'] = 'Bearer ' + token// token + } + + // headers中配置serialize为true开启序列化 + if (config.method === 'post' && config.headers.serialize) { + config.data = serialize(config.data) + delete config.data.serialize + } + + if (config.method === 'get') { + config.paramsSerializer = function(params) { + return qs.stringify(params, { arrayFormat: 'repeat' }) + } + } + return config +}, error => { + return Promise.reject(error) +}) + +// HTTPresponse拦截 +axios.interceptors.response.use(res => { + NProgress.done() + const status = Number(res.status) || 200 + const message = res.data.msg || errorCode[status] || errorCode['default'] + if (status == 401){ + window.open("http://localhost:3000/") + } + + // 后台定义 424 针对令牌过去的特殊响应码 + if (status === 424) { + ElMessageBox.confirm('令牌状态已过期,请点击重新登录', '系统提示', { + confirmButtonText: '重新登录', + cancelButtonText: '取消', + type: 'warning' + } + ).then(() => { + store.dispatch('LogOut').then(() => { + // 刷新登录页面,避免多次弹框 + window.location.reload() + }) + }).catch(() => { + }) + return + } + + if (status !== 200 || res.data.code === 1) { + ElMessage({ + message: message, + type: 'error' + }) + return Promise.reject(new Error(message)) + } + + return res +}, error => { + // 处理 503 网络异常 + console.log(error) + if (error.response.status === 503) { + ElMessage({ + message: error.response.data.msg, + type: 'error' + }) + } + NProgress.done() + return Promise.reject(new Error(error)) +}) + +export default axios diff --git a/summer-ospp/2023/pig/vue/src/store/modules/user.js b/summer-ospp/2023/pig/vue/src/store/modules/user.js new file mode 100644 index 000000000..142ae1891 --- /dev/null +++ b/summer-ospp/2023/pig/vue/src/store/modules/user.js @@ -0,0 +1,225 @@ +import { setToken, setRefreshToken } from '@/util/auth' +import { getStore, setStore } from '@/util/store' +import { ssoLogin,loginByMobile, loginByUsername, getUserInfo, logout, refreshToken } from '@/api/login' +import { deepClone, encryption } from '@/util' +import { formatPath } from '@/router/avue-router' +import { getMenu } from '@/api/admin/menu' +const user = { + state: { + userInfo: getStore({ + name: 'userInfo' + }) || {}, + permissions: getStore({ + name: 'permissions' + }) || [], + roles: [], + menu: getStore({ + name: 'menu' + }) || [], + menuAll: getStore({ name: 'menuAll' }) || [], + access_token: getStore({ + name: 'access_token' + }) || '', + refresh_token: getStore({ + name: 'refresh_token' + }) || '' + }, + actions: { + // SSO单点登陆 + SSOLogin({ commit }, myParam,service) { + return new Promise((resolve, reject) => { + ssoLogin(myParam,service).then(response => { + const data = response.data.data + console.log(data) + commit('SET_ACCESS_TOKEN', data.access_token) + commit('SET_REFRESH_TOKEN', data.refresh_token) + commit('CLEAR_LOCK') + resolve() + }).catch(error => { + reject(error) + }) + }) + }, + // 根据用户名登录 + LoginByUsername({ commit }, userInfo) { + const user = encryption({ + data: userInfo, + key: 'thanks,pig4cloud', + param: ['password'] + }) + return new Promise((resolve, reject) => { + loginByUsername(user.username, user.password, user.code, user.randomStr).then(response => { + const data = response.data + commit('SET_ACCESS_TOKEN', data.access_token) + commit('SET_REFRESH_TOKEN', data.refresh_token) + commit('CLEAR_LOCK') + resolve() + }).catch(error => { + reject(error) + }) + }) + }, + // 根据手机号登录 + LoginByPhone({ commit }, smsForm) { + return new Promise((resolve, reject) => { + loginByMobile(smsForm).then(response => { + const data = response.data + commit('SET_ACCESS_TOKEN', data.access_token) + commit('SET_REFRESH_TOKEN', data.refresh_token) + commit('CLEAR_LOCK') + resolve() + }).catch(error => { + reject(error) + }) + }) + }, + + // 刷新token + RefreshToken({ commit, state }) { + return new Promise((resolve, reject) => { + refreshToken(state.refresh_token).then(response => { + const data = response.data + commit('SET_ACCESS_TOKEN', data.access_token) + commit('SET_REFRESH_TOKEN', data.refresh_token) + commit('CLEAR_LOCK') + resolve() + }).catch(error => { + reject(error) + }) + }) + }, + // 查询用户信息 + GetUserInfo({ commit }) { + return new Promise((resolve, reject) => { + getUserInfo().then((res) => { + const data = res.data.data || {} + commit('SET_USER_INFO', data.sysUser) + commit('SET_ROLES', data.roles || []) + commit('SET_PERMISSIONS', data.permissions || []) + resolve(data) + }).catch(() => { + reject() + }) + }) + }, + // 登出 + LogOut({ commit }) { + return new Promise((resolve, reject) => { + logout().then(() => { + commit('SET_MENUALL_NULL', []) + commit('SET_MENU', []) + commit('SET_PERMISSIONS', []) + commit('SET_USER_INFO', {}) + commit('SET_ACCESS_TOKEN', '') + commit('SET_REFRESH_TOKEN', '') + commit('SET_ROLES', []) + commit('DEL_ALL_TAG') + commit('CLEAR_LOCK') + resolve() + }).catch(error => { + reject(error) + }) + }) + }, + // 注销session + FedLogOut({ commit }) { + return new Promise(resolve => { + commit('SET_MENU', []) + commit('SET_MENUALL_NULL', []) + commit('SET_PERMISSIONS', []) + commit('SET_USER_INFO', {}) + commit('SET_ACCESS_TOKEN', '') + commit('SET_REFRESH_TOKEN', '') + commit('SET_ROLES', []) + commit('DEL_ALL_TAG') + commit('CLEAR_LOCK') + resolve() + }) + }, + // 获取系统菜单 + GetMenu({ commit }, obj = {}) { + // 记录用户点击顶部信息,保证刷新的时候不丢失 + commit('LIKE_TOP_MENUID', obj) + return new Promise(resolve => { + getMenu(obj.id).then((res) => { + const data = res.data.data + const menu = deepClone(data) + menu.forEach(ele => formatPath(ele, true)) + commit('SET_MENUALL', menu) + commit('SET_MENU', menu) + resolve(menu) + }) + }) + }, + //顶部菜单 + GetTopMenu() { + return new Promise(resolve => { + resolve([]) + }) + } + }, + mutations: { + SET_ACCESS_TOKEN: (state, access_token) => { + state.access_token = access_token + setToken(access_token) + setStore({ + name: 'access_token', + content: state.access_token, + type: 'session' + }) + }, + SET_REFRESH_TOKEN: (state, rfToken) => { + state.refresh_token = rfToken + setRefreshToken(rfToken) + setStore({ + name: 'refresh_token', + content: state.refresh_token, + type: 'session' + }) + }, + SET_USER_INFO: (state, userInfo) => { + state.userInfo = userInfo + setStore({ + name: 'userInfo', + content: userInfo, + type: 'session' + }) + }, + SET_MENUALL: (state, menuAll) => { + const menu = state.menuAll + menuAll.forEach(ele => { + if (!menu.find(item => item.label === ele.label && item.path === ele.path)) { + menu.push(ele) + } + }) + state.menuAll = menu + setStore({ name: 'menuAll', content: state.menuAll }) + }, + SET_MENUALL_NULL: (state) => { + state.menuAll = [] + setStore({ name: 'menuAll', content: state.menuAll }) + }, + SET_MENU: (state, menu) => { + state.menu = menu + setStore({ name: 'menu', content: state.menu }) + }, + SET_ROLES: (state, roles) => { + state.roles = roles + }, + SET_PERMISSIONS: (state, permissions) => { + const list = {} + for (let i = 0; i < permissions.length; i++) { + list[permissions[i]] = true + } + + state.permissions = list + setStore({ + name: 'permissions', + content: list, + type: 'session' + }) + } + } + +} +export default user