|
|
# 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
|
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
|
<!--
|
|
|
~ 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.
|
|
|
-->
|
|
|
|
|
|
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
|
|
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
|
|
<modelVersion>4.0.0</modelVersion>
|
|
|
<parent>
|
|
|
<groupId>com.pig4cloud</groupId>
|
|
|
<artifactId>pig</artifactId>
|
|
|
<version>3.6.7</version>
|
|
|
</parent>
|
|
|
|
|
|
<artifactId>pig-auth</artifactId>
|
|
|
<packaging>jar</packaging>
|
|
|
|
|
|
<description>pig 认证授权中心,基于 spring security oAuth2</description>
|
|
|
|
|
|
<dependencies>
|
|
|
<dependency>
|
|
|
<groupId>io.jsonwebtoken</groupId>
|
|
|
<artifactId>jjwt</artifactId>
|
|
|
<version>0.7.0</version>
|
|
|
</dependency>
|
|
|
<!--注册中心客户端-->
|
|
|
<dependency>
|
|
|
<groupId>com.alibaba.cloud</groupId>
|
|
|
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
|
|
|
</dependency>
|
|
|
<!--配置中心客户端-->
|
|
|
<dependency>
|
|
|
<groupId>com.alibaba.cloud</groupId>
|
|
|
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
|
|
|
</dependency>
|
|
|
<!--断路器依赖-->
|
|
|
<dependency>
|
|
|
<groupId>com.pig4cloud</groupId>
|
|
|
<artifactId>pig-common-feign</artifactId>
|
|
|
</dependency>
|
|
|
<!--upms api、model 模块-->
|
|
|
<dependency>
|
|
|
<groupId>com.pig4cloud</groupId>
|
|
|
<artifactId>pig-upms-api</artifactId>
|
|
|
</dependency>
|
|
|
<dependency>
|
|
|
<groupId>com.pig4cloud</groupId>
|
|
|
<artifactId>pig-common-security</artifactId>
|
|
|
</dependency>
|
|
|
<dependency>
|
|
|
<groupId>org.springframework.boot</groupId>
|
|
|
<artifactId>spring-boot-starter-security</artifactId>
|
|
|
</dependency>
|
|
|
<!--freemarker-->
|
|
|
<dependency>
|
|
|
<groupId>org.springframework.boot</groupId>
|
|
|
<artifactId>spring-boot-starter-freemarker</artifactId>
|
|
|
</dependency>
|
|
|
<!--undertow容器-->
|
|
|
<dependency>
|
|
|
<groupId>org.springframework.boot</groupId>
|
|
|
<artifactId>spring-boot-starter-undertow</artifactId>
|
|
|
</dependency>
|
|
|
<!-- log -->
|
|
|
<dependency>
|
|
|
<groupId>com.pig4cloud</groupId>
|
|
|
<artifactId>pig-common-log</artifactId>
|
|
|
</dependency>
|
|
|
</dependencies>
|
|
|
|
|
|
<build>
|
|
|
<plugins>
|
|
|
<plugin>
|
|
|
<groupId>org.springframework.boot</groupId>
|
|
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
|
|
</plugin>
|
|
|
<plugin>
|
|
|
<groupId>io.fabric8</groupId>
|
|
|
<artifactId>docker-maven-plugin</artifactId>
|
|
|
</plugin>
|
|
|
</plugins>
|
|
|
</build>
|
|
|
|
|
|
</project>
|
|
|
|
|
|
```
|
|
|
|
|
|
#### 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<OAuth2AccessTokenResponse> accessTokenHttpResponseConverter = new OAuth2AccessTokenResponseHttpMessageConverter();
|
|
|
|
|
|
private final AuthenticationFailureHandler authenticationFailureHandler = new PigAuthenticationFailureEventHandler();
|
|
|
|
|
|
private final OAuth2AuthorizationService authorizationService;
|
|
|
|
|
|
private final RemoteClientDetailsService clientDetailsService;
|
|
|
|
|
|
private final RedisTemplate<String, Object> 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<String> authorizedScopes = StringUtils.commaDelimitedListToSet(clientDetails.getScope());
|
|
|
modelAndView.addObject("clientId", clientId);
|
|
|
modelAndView.addObject("state", state);
|
|
|
modelAndView.addObject("scopeList", authorizedScopes);
|
|
|
modelAndView.addObject("principalName", principal.getName());
|
|
|
modelAndView.setViewName("ftl/confirm");
|
|
|
return modelAndView;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 退出并删除token
|
|
|
*
|
|
|
* @param authHeader Authorization
|
|
|
*/
|
|
|
@DeleteMapping("/logout")
|
|
|
public R<Boolean> logout(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<String, Object> 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<Boolean> removeToken(@PathVariable("token") String token) {
|
|
|
OAuth2Authorization authorization = authorizationService.findByToken(token, OAuth2TokenType.ACCESS_TOKEN);
|
|
|
if (authorization == null) {
|
|
|
return R.ok();
|
|
|
}
|
|
|
|
|
|
OAuth2Authorization.Token<OAuth2AccessToken> accessToken = authorization.getAccessToken();
|
|
|
if (accessToken == null || StrUtil.isBlank(accessToken.getToken().getTokenValue())) {
|
|
|
return R.ok();
|
|
|
}
|
|
|
// 清空用户信息
|
|
|
cacheManager.getCache(CacheConstants.USER_DETAILS).evict(authorization.getPrincipalName());
|
|
|
// 清空access token
|
|
|
authorizationService.remove(authorization);
|
|
|
// 处理自定义退出事件,保存相关日志
|
|
|
SpringContextHolder.publishEvent(new LogoutSuccessEvent(new PreAuthenticatedAuthenticationToken(
|
|
|
authorization.getPrincipalName(), authorization.getRegisteredClientId())));
|
|
|
return R.ok();
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 查询token
|
|
|
*
|
|
|
* @param params 分页参数
|
|
|
* @return
|
|
|
*/
|
|
|
@Inner
|
|
|
@PostMapping("/page")
|
|
|
public R<Page> tokenList(@RequestBody Map<String, Object> 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<String> keys = redisTemplate.keys(key);
|
|
|
List<String> pages = keys.stream().skip((current - 1) * size).limit(size).collect(Collectors.toList());
|
|
|
Page result = new Page(current, size);
|
|
|
|
|
|
List<TokenVo> tokenVoList = redisTemplate.opsForValue().multiGet(pages).stream().map(obj -> {
|
|
|
OAuth2Authorization authorization = (OAuth2Authorization) obj;
|
|
|
TokenVo tokenVo = new TokenVo();
|
|
|
tokenVo.setClientId(authorization.getRegisteredClientId());
|
|
|
tokenVo.setId(authorization.getId());
|
|
|
tokenVo.setUsername(authorization.getPrincipalName());
|
|
|
OAuth2Authorization.Token<OAuth2AccessToken> accessToken = authorization.getAccessToken();
|
|
|
tokenVo.setAccessToken(accessToken.getToken().getTokenValue());
|
|
|
|
|
|
String 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<Map<String, String>> getToken(String ticket, String service) {
|
|
|
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
|
|
PigUser pigUser = (PigUser) authentication.getPrincipal();
|
|
|
Map<String, String> 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<String, Object> 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
|
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
|
<!--
|
|
|
~ 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.
|
|
|
-->
|
|
|
|
|
|
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
|
|
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
|
|
<modelVersion>4.0.0</modelVersion>
|
|
|
<parent>
|
|
|
<groupId>com.pig4cloud</groupId>
|
|
|
<artifactId>pig-common</artifactId>
|
|
|
<version>3.6.7</version>
|
|
|
</parent>
|
|
|
|
|
|
<artifactId>pig-common-security</artifactId>
|
|
|
<packaging>jar</packaging>
|
|
|
|
|
|
<description>pig 安全工具类</description>
|
|
|
|
|
|
|
|
|
<dependencies>
|
|
|
<dependency>
|
|
|
<groupId>org.springframework.session</groupId>
|
|
|
<artifactId>spring-session-data-redis</artifactId>
|
|
|
</dependency>
|
|
|
<!-- spring security cas-->
|
|
|
<dependency>
|
|
|
<groupId>org.springframework.security</groupId>
|
|
|
<artifactId>spring-security-cas</artifactId>
|
|
|
</dependency>
|
|
|
<!--工具类核心包-->
|
|
|
<dependency>
|
|
|
<groupId>com.pig4cloud</groupId>
|
|
|
<artifactId>pig-common-core</artifactId>
|
|
|
</dependency>
|
|
|
<dependency>
|
|
|
<groupId>cn.hutool</groupId>
|
|
|
<artifactId>hutool-extra</artifactId>
|
|
|
</dependency>
|
|
|
<!--UPMS API-->
|
|
|
<dependency>
|
|
|
<groupId>com.pig4cloud</groupId>
|
|
|
<artifactId>pig-upms-api</artifactId>
|
|
|
</dependency>
|
|
|
<!--common utils-->
|
|
|
<dependency>
|
|
|
<groupId>org.springframework.cloud</groupId>
|
|
|
<artifactId>spring-cloud-commons</artifactId>
|
|
|
</dependency>
|
|
|
<!--feign 工具类-->
|
|
|
<dependency>
|
|
|
<groupId>org.springframework.cloud</groupId>
|
|
|
<artifactId>spring-cloud-starter-openfeign</artifactId>
|
|
|
</dependency>
|
|
|
<dependency>
|
|
|
<groupId>org.springframework.security</groupId>
|
|
|
<artifactId>spring-security-oauth2-jose</artifactId>
|
|
|
</dependency>
|
|
|
<dependency>
|
|
|
<groupId>org.springframework.security</groupId>
|
|
|
<artifactId>spring-security-oauth2-authorization-server</artifactId>
|
|
|
<version>${spring.authorization.version}</version>
|
|
|
</dependency>
|
|
|
<dependency>
|
|
|
<groupId>org.springframework</groupId>
|
|
|
<artifactId>spring-webmvc</artifactId>
|
|
|
</dependency>
|
|
|
</dependencies>
|
|
|
</project>
|
|
|
|
|
|
```
|
|
|
|
|
|
#### 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
|
|
|
* <p>
|
|
|
* 资源服务注解
|
|
|
*/
|
|
|
@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<CasAssertionAuthenticationToken> {
|
|
|
|
|
|
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<UserInfo> result = remoteUserService.saveIfNotExist(token.getAssertion().getPrincipal().getAttributes());
|
|
|
return getUserDetails(result, username);
|
|
|
}
|
|
|
|
|
|
private UserDetails getUserDetails(R<UserInfo> 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
|
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
|
<!--
|
|
|
~ 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.
|
|
|
-->
|
|
|
|
|
|
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
|
|
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
|
|
<modelVersion>4.0.0</modelVersion>
|
|
|
<parent>
|
|
|
<groupId>com.pig4cloud</groupId>
|
|
|
<artifactId>pig-upms</artifactId>
|
|
|
<version>3.6.7</version>
|
|
|
</parent>
|
|
|
|
|
|
<artifactId>pig-upms-biz</artifactId>
|
|
|
<packaging>jar</packaging>
|
|
|
|
|
|
<description>pig 通用用户权限管理系统业务处理模块</description>
|
|
|
|
|
|
<dependencies>
|
|
|
<dependency>
|
|
|
<groupId>com.google.guava</groupId>
|
|
|
<artifactId>guava</artifactId>
|
|
|
<version>29.0-jre</version>
|
|
|
</dependency>
|
|
|
<!--upms api、model 模块-->
|
|
|
<dependency>
|
|
|
<groupId>com.pig4cloud</groupId>
|
|
|
<artifactId>pig-upms-api</artifactId>
|
|
|
</dependency>
|
|
|
<!--文件管理-->
|
|
|
<dependency>
|
|
|
<groupId>com.pig4cloud.plugin</groupId>
|
|
|
<artifactId>oss-spring-boot-starter</artifactId>
|
|
|
</dependency>
|
|
|
<!--feign 调用-->
|
|
|
<dependency>
|
|
|
<groupId>com.pig4cloud</groupId>
|
|
|
<artifactId>pig-common-feign</artifactId>
|
|
|
</dependency>
|
|
|
<!--安全模块-->
|
|
|
<dependency>
|
|
|
<groupId>com.pig4cloud</groupId>
|
|
|
<artifactId>pig-common-security</artifactId>
|
|
|
</dependency>
|
|
|
<!--日志处理-->
|
|
|
<dependency>
|
|
|
<groupId>com.pig4cloud</groupId>
|
|
|
<artifactId>pig-common-log</artifactId>
|
|
|
</dependency>
|
|
|
<!--接口文档-->
|
|
|
<dependency>
|
|
|
<groupId>com.pig4cloud</groupId>
|
|
|
<artifactId>pig-common-swagger</artifactId>
|
|
|
</dependency>
|
|
|
<!-- orm 模块-->
|
|
|
<dependency>
|
|
|
<groupId>com.baomidou</groupId>
|
|
|
<artifactId>mybatis-plus-boot-starter</artifactId>
|
|
|
</dependency>
|
|
|
<dependency>
|
|
|
<groupId>com.mysql</groupId>
|
|
|
<artifactId>mysql-connector-j</artifactId>
|
|
|
</dependency>
|
|
|
<!--注册中心客户端-->
|
|
|
<dependency>
|
|
|
<groupId>com.alibaba.cloud</groupId>
|
|
|
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
|
|
|
</dependency>
|
|
|
<!--配置中心客户端-->
|
|
|
<dependency>
|
|
|
<groupId>com.alibaba.cloud</groupId>
|
|
|
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
|
|
|
</dependency>
|
|
|
<!-- 阿里云短信下发 -->
|
|
|
<dependency>
|
|
|
<groupId>io.springboot.sms</groupId>
|
|
|
<artifactId>aliyun-sms-spring-boot-starter</artifactId>
|
|
|
</dependency>
|
|
|
<!--xss 过滤-->
|
|
|
<dependency>
|
|
|
<groupId>com.pig4cloud</groupId>
|
|
|
<artifactId>pig-common-xss</artifactId>
|
|
|
</dependency>
|
|
|
<!--undertow容器-->
|
|
|
<dependency>
|
|
|
<groupId>org.springframework.boot</groupId>
|
|
|
<artifactId>spring-boot-starter-undertow</artifactId>
|
|
|
</dependency>
|
|
|
</dependencies>
|
|
|
|
|
|
<build>
|
|
|
<plugins>
|
|
|
<plugin>
|
|
|
<groupId>org.springframework.boot</groupId>
|
|
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
|
|
</plugin>
|
|
|
<plugin>
|
|
|
<groupId>io.fabric8</groupId>
|
|
|
<artifactId>docker-maven-plugin</artifactId>
|
|
|
</plugin>
|
|
|
</plugins>
|
|
|
<resources>
|
|
|
<resource>
|
|
|
<directory>src/main/resources</directory>
|
|
|
<filtering>true</filtering>
|
|
|
<excludes>
|
|
|
<exclude>**/*.xlsx</exclude>
|
|
|
<exclude>**/*.xls</exclude>
|
|
|
</excludes>
|
|
|
</resource>
|
|
|
<resource>
|
|
|
<directory>src/main/resources</directory>
|
|
|
<filtering>false</filtering>
|
|
|
<includes>
|
|
|
<include>**/*.xlsx</include>
|
|
|
<include>**/*.xls</include>
|
|
|
</includes>
|
|
|
</resource>
|
|
|
</resources>
|
|
|
</build>
|
|
|
|
|
|
</project>
|
|
|
|
|
|
```
|
|
|
|
|
|
#### 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<UserInfoVO> info() {
|
|
|
String username = SecurityUtils.getUser().getUsername();
|
|
|
SysUser user = userService.getOne(Wrappers.<SysUser>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<UserInfo> info(@PathVariable String username) {
|
|
|
SysUser user = userService.getOne(Wrappers.<SysUser>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<UserInfo> save_sso(@RequestBody Map<String, Object> attributes) {
|
|
|
String username = (String) attributes.getOrDefault("username", "pig");
|
|
|
// 判断用户名是否存在
|
|
|
SysUser sysUser = userService.getOne(Wrappers.<SysUser>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<List<Long>> listUserIdByDeptIds(@RequestParam("deptIds") Set<Long> deptIds) {
|
|
|
return R.ok(userService.listUserIdByDeptIds(deptIds));
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 通过ID查询用户信息
|
|
|
*
|
|
|
* @param id ID
|
|
|
* @return 用户信息
|
|
|
*/
|
|
|
@GetMapping("/{id:\\d+}")
|
|
|
public R<UserVO> user(@PathVariable Long id) {
|
|
|
return R.ok(userService.getUserVoById(id));
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 判断用户是否存在
|
|
|
*
|
|
|
* @param userDTO 查询条件
|
|
|
* @return
|
|
|
*/
|
|
|
@Inner(false)
|
|
|
@GetMapping("/check/exist")
|
|
|
public R<Boolean> isExist(UserDTO userDTO) {
|
|
|
List<SysUser> 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<Boolean> 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<Boolean> 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<Boolean> updateUser(@Valid @RequestBody UserDTO userDto) {
|
|
|
return userService.updateUser(userDto);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 分页查询用户
|
|
|
*
|
|
|
* @param page 参数集
|
|
|
* @param userDTO 查询参数列表
|
|
|
* @return 用户集合
|
|
|
*/
|
|
|
@GetMapping("/page")
|
|
|
public R<IPage<UserVO>> 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<Boolean> updateUserInfo(@Valid @RequestBody UserDTO userDto) {
|
|
|
userDto.setUsername(SecurityUtils.getUser().getUsername());
|
|
|
return userService.updateUserInfo(userDto);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* @param username 用户名称
|
|
|
* @return 上级部门用户列表
|
|
|
*/
|
|
|
@GetMapping("/ancestor/{username}")
|
|
|
public R<List<SysUser>> 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<UserExcelVO> 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<UserExcelVO> 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<String, Object> 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
|
|
|
<template>
|
|
|
<el-form
|
|
|
ref="loginForm"
|
|
|
class="login-form"
|
|
|
status-icon
|
|
|
:rules="loginRules"
|
|
|
:model="loginForm"
|
|
|
label-width="0"
|
|
|
>
|
|
|
<el-form-item prop="username">
|
|
|
<el-input
|
|
|
v-model="loginForm.username"
|
|
|
auto-complete="off"
|
|
|
placeholder="请输入用户名"
|
|
|
@keyup.enter.native="handleLogin"
|
|
|
>
|
|
|
<template #prefix>
|
|
|
<i class="icon-yonghu"></i>
|
|
|
</template>
|
|
|
</el-input>
|
|
|
</el-form-item>
|
|
|
<el-form-item prop="password">
|
|
|
<el-input
|
|
|
v-model="loginForm.password"
|
|
|
size="small"
|
|
|
type="password"
|
|
|
auto-complete="off"
|
|
|
show-password
|
|
|
placeholder="请输入密码"
|
|
|
@keyup.enter.native="handleLogin"
|
|
|
>
|
|
|
<template #prefix>
|
|
|
<i class="icon-mima"></i>
|
|
|
</template>
|
|
|
|
|
|
</el-input>
|
|
|
</el-form-item>
|
|
|
<el-form-item v-if="website.validateCode" prop="code">
|
|
|
<el-input
|
|
|
v-model="loginForm.code"
|
|
|
:maxlength="code.len"
|
|
|
auto-complete="off"
|
|
|
placeholder="请输入验证码"
|
|
|
@keyup.enter.native="handleLogin"
|
|
|
>
|
|
|
<template #prefix>
|
|
|
<i class="icon-yanzhengma"></i>
|
|
|
</template>
|
|
|
<template #append>
|
|
|
<div class="login-code">
|
|
|
<span
|
|
|
v-if="code.type === 'text'"
|
|
|
class="login-code-img"
|
|
|
@click="refreshCode"
|
|
|
>{{ code.value }}</span
|
|
|
>
|
|
|
<img
|
|
|
v-else
|
|
|
:src="code.src"
|
|
|
class="login-code-img"
|
|
|
@click="refreshCode"
|
|
|
/>
|
|
|
</div>
|
|
|
</template>
|
|
|
</el-input>
|
|
|
</el-form-item>
|
|
|
<el-form-item>
|
|
|
<el-button
|
|
|
type="primary"
|
|
|
class="login-submit"
|
|
|
@click.native.prevent="handleLogin"
|
|
|
>登录
|
|
|
</el-button
|
|
|
>
|
|
|
</el-form-item>
|
|
|
|
|
|
</el-form>
|
|
|
</template>
|
|
|
|
|
|
<script>
|
|
|
import { randomLenNum } from '@/util'
|
|
|
import { mapGetters } from 'vuex'
|
|
|
// import {ssoLogin} from '@/api/login'
|
|
|
export default {
|
|
|
name: 'userlogin',
|
|
|
data() {
|
|
|
return {
|
|
|
loginForm: {
|
|
|
username: 'admin',
|
|
|
password: '123456',
|
|
|
code: '',
|
|
|
randomStr: ''
|
|
|
},
|
|
|
checked: false,
|
|
|
code: {
|
|
|
src: '/code',
|
|
|
value: '',
|
|
|
len: 4,
|
|
|
type: 'image'
|
|
|
},
|
|
|
loginRules: {
|
|
|
username: [
|
|
|
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
|
|
{ pattern: /^([a-z\u4e00-\u9fa5\d]*?)$/, message: '请输入小写字母', trigger: 'blur' }
|
|
|
],
|
|
|
password: [
|
|
|
{ required: true, message: '请输入密码', trigger: 'blur' },
|
|
|
{ min: 6, message: '密码长度最少为6位', trigger: 'blur' }
|
|
|
],
|
|
|
code: [{ required: true, message: '请输入验证码', trigger: 'blur' }]
|
|
|
}
|
|
|
}
|
|
|
},
|
|
|
created() {
|
|
|
var params = new URLSearchParams(window.location.search);
|
|
|
var myParam = params.get('ticket');
|
|
|
var service = params.get('service');
|
|
|
if(myParam!=''&&myParam!=null){
|
|
|
this.ssoLogin(myParam,service)
|
|
|
}
|
|
|
},
|
|
|
updated(){
|
|
|
var params = new URLSearchParams(window.location.search);
|
|
|
var myParam = params.get('ticket');
|
|
|
var service = params.get('service');
|
|
|
if(myParam!=''&&myParam!=null){
|
|
|
this.ssoLogin(myParam,service)
|
|
|
}
|
|
|
},
|
|
|
computed: {
|
|
|
...mapGetters(['tagWel', 'website'])
|
|
|
},
|
|
|
methods: {
|
|
|
async ssoLogin(myParam,service){
|
|
|
this.$store
|
|
|
.dispatch('SSOLogin', myParam,service)
|
|
|
.then(() => {
|
|
|
this.$router.push({ path: this.tagWel.value })
|
|
|
})
|
|
|
.catch(() => {
|
|
|
this.refreshCode()
|
|
|
})
|
|
|
},
|
|
|
refreshCode() {
|
|
|
this.loginForm.code = ''
|
|
|
this.loginForm.randomStr = randomLenNum(this.code.len, true)
|
|
|
this.code.type === 'text'
|
|
|
? (this.code.value = randomLenNum(this.code.len))
|
|
|
: (this.code.src = `${this.baseUrl}/code?randomStr=${this.loginForm.randomStr}`)
|
|
|
},
|
|
|
handleLogin() {
|
|
|
this.$refs.loginForm.validate(valid => {
|
|
|
if (valid) {
|
|
|
this.$store
|
|
|
.dispatch('LoginByUsername', this.loginForm)
|
|
|
.then(() => {
|
|
|
this.$router.push({ path: this.tagWel.value })
|
|
|
})
|
|
|
.catch(() => {
|
|
|
this.refreshCode()
|
|
|
})
|
|
|
}
|
|
|
})
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
</script>
|
|
|
|
|
|
<style></style>
|
|
|
|
|
|
```
|
|
|
|
|
|
#### 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
|
|
|
```
|