Security导包:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
模拟Redis存储登录信息:
public class CacheEntity implements Serializable {
private Object value;
/**
* 保存的时间戳
*/
private long gmtModify;
/**
* 过期时间
*/
private int expire;
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = value;
}
public long getGmtModify() {
return gmtModify;
}
public void setGmtModify(long gmtModify) {
this.gmtModify = gmtModify;
}
public int getExpire() {
return expire;
}
public void setExpire(int expire) {
this.expire = expire;
}
public CacheEntity(Object value, long gmtModify, int expire) {
this.value = value;
this.gmtModify = gmtModify;
this.expire = expire;
}
}
@Slf4j
public class LocalCache {
/**
* 默认的缓存容量
*/
private static final int DEFAULT_CAPACITY = 512;
/**
* 最大容量
*/
private static final int MAX_CAPACITY = 100000;
/**
* 刷新缓存的频率
*/
private static final int MONITOR_DURATION = 2;
// 启动监控线程
static {
new Thread(new TimeoutTimerThread()).start();
}
// 内部类方式实现单例
private static class LocalCacheInstance {
private static final LocalCache INSTANCE = new LocalCache();
}
public static LocalCache getInstance() {
return LocalCacheInstance.INSTANCE;
}
private LocalCache() {
}
/**
* 使用默认容量创建一个Map
*/
private static Map<String, CacheEntity> cache = new ConcurrentHashMap<>(DEFAULT_CAPACITY);
/**
* 将key-value保存到本地缓存并制定该缓存的过期时间
*
* @param key
* @param value
* @param expireTime 过期时间,如果是-1 则表示永不过期
* @param <T>
* @return
*/
public <T> boolean putValue(String key, T value, int expireTime) {
return putCloneValue(key, value, expireTime);
}
/**
* 将值通过序列化clone 处理后保存到缓存中,可以解决值引用的问题
*
* @param key
* @param value
* @param expireTime
* @param <T>
* @return
*/
private <T> boolean putCloneValue(String key, T value, int expireTime) {
try {
if (cache.size() >= MAX_CAPACITY) {
return false;
}
// 序列化赋值
CacheEntity entityClone = clone(new CacheEntity(value, System.nanoTime(), expireTime));
cache.put(key, entityClone);
return true;
} catch (Exception e) {
log.error("添加缓存失败:{}", e.getMessage());
}
return false;
}
/**
* 序列化 克隆处理
*
* @param object
* @param <E>
* @return
*/
private <E extends Serializable> E clone(E object) {
E cloneObject = null;
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(object);
oos.close();
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
cloneObject = (E) ois.readObject();
ois.close();
} catch (Exception e) {
log.error("缓存序列化失败:{}", e.getMessage());
}
return cloneObject;
}
/**
* 从本地缓存中获取key对应的值,如果该值不存则则返回null
*
* @param key
* @return
*/
public Object getValue(String key) {
if (CollectionUtils.isEmpty(cache)) {
return null;
}
CacheEntity cacheEntity = cache.get(key);
if (ObjectUtils.isEmpty(cacheEntity)) {
return null;
}
return cacheEntity.getValue();
}
public void remove(String key) {
if (CollectionUtils.isEmpty(cache)) {
return;
}
CacheEntity cacheEntity = cache.get(key);
if (ObjectUtils.isEmpty(cacheEntity)) {
return;
}
cache.remove(key);
}
/**
* 清空所有
*/
public void clear() {
cache.clear();
}
/**
* 过期处理线程
*/
static class TimeoutTimerThread implements Runnable {
@Override
public void run() {
while (true) {
try {
TimeUnit.SECONDS.sleep(MONITOR_DURATION);
checkTime();
} catch (Exception e) {
log.error("过期缓存清理失败:{}", e.getMessage());
}
}
}
/**
* 过期缓存的具体处理方法 * * @throws Exception
*/
private void checkTime() throws Exception {
// 开始处理过期
for (String key : cache.keySet()) {
CacheEntity tce = cache.get(key);
long timoutTime = TimeUnit.NANOSECONDS.toSeconds(System.nanoTime() - tce.getGmtModify());
// 过期时间 : timoutTime
if (tce.getExpire() > timoutTime) {
continue;
}
log.info(" 清除过期缓存 :" + key);
//清除过期缓存和删除对应的缓存队列
cache.remove(key);
}
}
}
}
权限枚举:
// 权限值是将二进制与十进制相互转换来判断的
public enum PermissionEnum {
GET_DEPARTMENT(1, "单位获取", "ROLE_GET_DEPARTMENT", 0x0000000000000001L),
INSERT_DEPARTMENT(2, "单位增加", "ROLE_INSERT_DEPARTMENT", 0x0000000000000002L),
UPDATE_DEPARTMENT(3, "单位修改", "ROLE_UPDATE_DEPARTMENT", 0x0000000000000004L),
DELETE_DEPARTMENT(4, "单位删除", "ROLE_DELETE_DEPARTMENT", 0x0000000000000008L),
;
private int id;
private String permissions;
private String permissionNames;
private Long value;
PermissionEnum(int id, String permissions, String permissionNames, Long value) {
this.id = id;
this.permissions = permissions;
this.permissionNames = permissionNames;
this.value = value;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getPermissions() {
return permissions;
}
public void setPermissions(String permissions) {
this.permissions = permissions;
}
public String getPermissionNames() {
return permissionNames;
}
public void setPermissionNames(String permissionNames) {
this.permissionNames = permissionNames;
}
public Long getValue() {
return value;
}
public void setValue(Long value) {
this.value = value;
}
public static List<GrantedAuthority> fromCode(Long code) {
List<GrantedAuthority> list = new ArrayList<>();
PermissionEnum[] codes = PermissionEnum.values();
for (PermissionEnum state : codes) {
if ((state.getValue() & code) > 0) {
list.add(new SimpleGrantedAuthority(state.getPermissionNames()));
}
}
return list;
}
public static List<PermissionEnum> getAuthList(Long code) {
List<PermissionEnum> list = new ArrayList<>();
PermissionEnum[] codes = PermissionEnum.values();
for (PermissionEnum state : codes) {
if ((state.getValue() & code) > 0) {
list.add(state);
}
}
return list;
}
// 获取权限值
public static Long getPermissionCode(Integer[] auths) {
Long code = 0x0000000000000000L;
PermissionEnum[] codes = PermissionEnum.values();
for (Integer auth : auths) {
for (PermissionEnum permissionCode : codes) {
if (auth.equals(permissionCode.getId())) {
code += permissionCode.getValue();
break;
}
}
}
return code;
}
// 获取权限数组
public static String[] getAuths(Long code) {
List<String> lists = new ArrayList<>();
PermissionEnum[] codes = PermissionEnum.values();
for (PermissionEnum state : codes) {
if ((state.getValue() & code) > 0) {
lists.add(state.getPermissions());
}
}
return lists.toArray(new String[lists.size()]);
}
// 获取权限值
public static Long getPermissionCode(String[] auths) {
Long code = 0x0000000000000000L;
PermissionEnum[] codes = PermissionEnum.values();
for (String auth : auths) {
for (PermissionEnum permissionCode : codes) {
if (auth.equals(permissionCode.getPermissions())) {
code += permissionCode.getValue();
break;
}
}
}
return code;
}
}
User实体类:
@Data
@Accessors(chain = true)
public class Users implements Serializable {
private Long userID;
private String userName;
private String userPassword;
private String userPhone;
private String userAddress;
private Integer userAllowErrCount;
private Integer userErrCount;
private Date userLastErrTime;
private Long userRoleID;
private Roles roles;
private Long userDepID;
private Department department;
private boolean userEnable;
}
权限反序列化:
public class CustomAuthorityDeserializer extends JsonDeserializer {
@Override
public Object deserialize(
JsonParser p, DeserializationContext deserializationContext
) throws IOException, JacksonException {
ObjectMapper mapper = (ObjectMapper) p.getCodec();
JsonNode jsonNode = mapper.readTree(p);
LinkedList<GrantedAuthority> grantedAuthorities = new LinkedList<>();
Iterator<JsonNode> elements = jsonNode.elements();
while (elements.hasNext()) {
JsonNode next = elements.next();
JsonNode authority = next.get("authority");
//将得到的值放入链表 最终返回该链表
grantedAuthorities.add(new SimpleGrantedAuthority(authority.asText()));
}
return grantedAuthorities;
}
}
用户详情类:
@JsonIgnoreProperties({"enabled", "accountNonExpired", "accountNonLocked", "credentialsNonExpired",
"username", "password"})
public class MyUserDetail extends Users implements UserDetails, Serializable {
List<? extends GrantedAuthority> authorities;
public MyUserDetail() {
}
public MyUserDetail(Users users, List<? extends GrantedAuthority> authList) {
this.setUserID(users.getUserID());
this.setUserName(users.getUserName());
this.setUserPassword(users.getUserPassword());
this.setUserDepID(users.getUserDepID());
this.setUserRoleID(users.getUserRoleID());
this.authorities = authList;
}
@Override
@JsonDeserialize(using = CustomAuthorityDeserializer.class)
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.authorities;
}
@Override
public String getPassword() {
return this.getUserPassword();
}
@Override
public String getUsername() {
return this.getUserName();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
用户详情实现类:
@Component
public class MyUserDetailServiceImpl implements UserDetailsService {
// 操作数据库,根据用户名称查询用户信息
private final UserMapper userMapper;
public MyUserDetailServiceImpl(UserMapper userMapper) {
this.userMapper = userMapper;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Users users = userMapper.getUserByName(username);
Optional.ofNullable(users).orElseThrow(() -> {
// 自定义的异常返回类和枚举
throw new CommonException(YIXGResultEnum.USER_NOT_EXIST.getCode(),
YIXGResultEnum.USER_NOT_EXIST.getMessage());
});
if (ObjectUtils.isEmpty(users.getUserRoleID())) {
throw new CommonException(YIXGResultEnum.USER_ROLE_NOT_EXIST.getCode(),
YIXGResultEnum.USER_ROLE_NOT_EXIST.getMessage());
}
List<GrantedAuthority> authorityList = PermissionEnum.fromCode(users.getRoles().getRolePermission());
return new MyUserDetail(users, authorityList);
}
}
拦截未登录请求:
/**
* 用户发起未登录的请求会被AuthorizationFilter拦截,并抛出AccessDeniedException异常。异常被AuthenticationEntryPoint
* 处理,默认会触发重定向到登录页。Spring Security开放了配置,允许我们自定义AuthenticationEntryPoint。
* 那么我们就通过自定义AuthenticationEntryPoint来取消重定向行为,将接口改为返回JSON信息。
*/
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(
HttpServletRequest request, HttpServletResponse response, AuthenticationException authException
) throws IOException, ServletException {
CommonResult commonResult = new CommonResult();
ObjectMapper objectMapper = new ObjectMapper();
commonResult.setCode(YIXGResultEnum.LOGIN_INVALID.getCode())
.setMessage(YIXGResultEnum.LOGIN_INVALID.getMessage());
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(commonResult));
response.getWriter().flush();
response.getWriter().close();
}
}
拦截没权限的请求:
public class MyAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(
HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException
) throws IOException, ServletException {
ObjectMapper objectMapper = new ObjectMapper();
CommonResult commonResult = new CommonResult();
commonResult.setCode(YIXGResultEnum.NO_PERMISSION.getCode())
.setMessage(YIXGResultEnum.NO_PERMISSION.getMessage());
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(commonResult));
response.getWriter().flush();
response.getWriter().close();
}
}
自定义拦截器,验证token信息:
public class MyAuthenticationTokenFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain
) throws ServletException, IOException {
// 从header中获取验证信息
String authHeader = request.getHeader(GlobalUtil.AUTHORIZATION);
if (ObjectUtils.isEmpty(authHeader)) {
filterChain.doFilter(request, response);
return;
}
this.doParse(request, response, filterChain, authHeader);
}
private void doParse(
HttpServletRequest request, HttpServletResponse response, FilterChain chain, String authHeader
) throws ServletException, IOException {
ObjectMapper objectMapper = new ObjectMapper();
// 如果认证码 以规定值开头
if (authHeader.startsWith(GlobalUtil.GRANT_TYPE)) {
// 提取token值
String token = authHeader.substring(GlobalUtil.GRANT_TYPE.length());
if (ObjectUtils.isEmpty(token)) {
chain.doFilter(request, response);
return;
}
// 通过token值从缓存中取用户信息
String userJson = (String) LocalCache.getInstance().getValue(token);
// 转换JSON对象
//JSONObject userJsonObject = JSON.parseObject(userJson);
// 判断是否空值
if (ObjectUtils.isEmpty(userJson)) {
// throw new CommonException(YIXGResultEnum.INVALID_TOKEN.getCode(),
// YIXGResultEnum.INVALID_TOKEN.getMsg());
chain.doFilter(request, response);
return;
}
// 转换MyUserDetail对象
MyUserDetail user = objectMapper.readValue(userJson, MyUserDetail.class);
//MyUserDetail user = JSON.toJavaObject(userJsonObject, MyUserDetail.class);
// MyUserDetail user = JSONObject.toJavaObject(userJsonObject, MyUserDetail.class);
// 转换 UP 对象放到上下文中
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
new UsernamePasswordAuthenticationToken(
user, user.getPassword(), user.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
chain.doFilter(request, response);
}
}
密码加密:
public class MyPasswordEncoder implements PasswordEncoder {
@Override
public String encode(CharSequence rawPassword) {
return MD5Util.md5((String) rawPassword);
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return encodedPassword.equalsIgnoreCase(MD5Util.md5((String) rawPassword));
}
}
登录成功:
@Component
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private final ObjectMapper objectMapper;
private final LogService logService;
private final UserService userService;
public MyAuthenticationSuccessHandler(
ObjectMapper objectMapper, LogService logService,
UserService userService
) {
this.objectMapper = objectMapper;
this.logService = logService;
this.userService = userService;
}
@Override
public void onAuthenticationSuccess(
HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authentication
) throws IOException, ServletException {
AuthenticationSuccessHandler.super.onAuthenticationSuccess(request, response, chain, authentication);
}
@Override
public void onAuthenticationSuccess(
HttpServletRequest request, HttpServletResponse response, Authentication authentication
) throws IOException, ServletException {
MyUserDetail user = (MyUserDetail) authentication.getPrincipal();
// 获取随机token 并存到Redis中
String token = UUID.randomUUID().toString().replaceAll("-", "");
LocalCache.getInstance().putValue(token, objectMapper.writeValueAsString(user), 60 * 60);
UserVO userVO = new UserVO();
userVO.setUserName(user.getUserName())
.setUserErrCount("0")
.setUserLastErrTime(null);
userService.updateUserErrCount(userVO);
LogVO logVO = new LogVO();
logVO.setLogOperateUser(user.getUserName())
.setLogContent("登录成功")
.setLogType("登录日志");
logService.addLog(logVO);
CommonResult commonResult = new CommonResult();
commonResult.setCode(YIXGResultEnum.OPERATE_SUCCESS.getCode())
.setMessage(YIXGResultEnum.OPERATE_SUCCESS.getMessage())
.setToken(token)
.setCurrentUser(user.getUserName())
.setCurrentUserId(user.getUserID());
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(commonResult));
response.getWriter().flush();
response.getWriter().close();
}
}
登录失败:
@Component
@Slf4j
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
private final ObjectMapper objectMapper;
private final UserService userService;
public MyAuthenticationFailureHandler(ObjectMapper objectMapper, UserService userService) {
this.objectMapper = objectMapper;
this.userService = userService;
}
@Override
public void onAuthenticationFailure(
HttpServletRequest request, HttpServletResponse response, AuthenticationException exception
) throws IOException, ServletException {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
CommonResult result = userService.getUserByUserName(
new UserVO().setUserName(request.getParameter("username")));
Users users = (Users) result.getObjectData();
if (Objects.equals(result.getCode(), YIXGResultEnum.OPERATE_SUCCESS.getCode())) {
UserVO userVO = new UserVO();
userVO.setUserName(users.getUserName())
.setUserErrCount(String.valueOf((users.getUserErrCount() + 1)))
.setUserLastErrTime(sdf.format(new Date()));
userService.updateUserErrCount(userVO);
}
CommonResult commonResult = new CommonResult();
commonResult.setCode(YIXGResultEnum.PASSWORD_OR_USERNAME_ERROR.getCode())
.setMessage(YIXGResultEnum.PASSWORD_OR_USERNAME_ERROR.getMessage());
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(commonResult));
response.getWriter().flush();
response.getWriter().close();
}
}
登出成功:
@Component
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
private final ObjectMapper objectMapper;
private final LogService logService;
public MyLogoutSuccessHandler(ObjectMapper objectMapper, LogService logService) {
this.objectMapper = objectMapper;
this.logService = logService;
}
@Override
public void onLogoutSuccess(
HttpServletRequest request, HttpServletResponse response, Authentication authentication
) throws IOException, ServletException {
String authHeader = request.getHeader(GlobalUtil.AUTHORIZATION);
String authToken = authHeader.substring(GlobalUtil.GRANT_TYPE.length());
String userJson = (String) LocalCache.getInstance().getValue(authToken);
if (ObjectUtils.isEmpty(userJson)) {
CommonResult commonResult = new CommonResult();
commonResult.setCode(YIXGResultEnum.OPERATE_FAILURE.getCode())
.setMessage(YIXGResultEnum.OPERATE_FAILURE.getMessage());
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(commonResult));
response.getWriter().flush();
response.getWriter().close();
return;
}
MyUserDetail user = objectMapper.readValue(userJson, MyUserDetail.class);
LocalCache.getInstance().putValue(authToken, "", 1);
LogVO logVO = new LogVO();
logVO.setLogOperateUser(user.getUserName())
.setLogContent("登出成功")
.setLogType("登录日志");
logService.addLog(logVO);
CommonResult commonResult = new CommonResult();
commonResult.setCode(YIXGResultEnum.OPERATE_SUCCESS.getCode())
.setMessage(YIXGResultEnum.OPERATE_SUCCESS.getMessage());
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(commonResult));
response.getWriter().flush();
response.getWriter().close();
}
}
Security核心配置:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
private final MyAuthenticationFailureHandler myAuthenticationFailureHandler;
private final MyLogoutSuccessHandler myLogoutSuccessHandler;
private final UserDetailsService userDetailsService;
public SecurityConfig(
MyAuthenticationSuccessHandler myAuthenticationSuccessHandler,
MyAuthenticationFailureHandler myAuthenticationFailureHandler,
MyLogoutSuccessHandler myLogoutSuccessHandler, UserDetailsService userDetailsService
) {
this.myAuthenticationSuccessHandler = myAuthenticationSuccessHandler;
this.myAuthenticationFailureHandler = myAuthenticationFailureHandler;
this.myLogoutSuccessHandler = myLogoutSuccessHandler;
this.userDetailsService = userDetailsService;
}
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration authenticationConfiguration
) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new MyPasswordEncoder();
}
@Bean
public MyAuthenticationTokenFilter myAuthenticationTokenFilter() {
return new MyAuthenticationTokenFilter();
}
@Bean
SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity
// 禁用basic明文验证
.httpBasic(Customizer.withDefaults())
// 基于 token ,不需要 csrf
.csrf(AbstractHttpConfigurer::disable)
// 禁用默认登录页
.formLogin(fl -> fl.loginProcessingUrl("/login")
.usernameParameter("username")
.passwordParameter("password")
.successHandler(myAuthenticationSuccessHandler)
.failureHandler(myAuthenticationFailureHandler)
.permitAll())
// 禁用默认登出页
.logout(lt -> lt.logoutSuccessHandler(myLogoutSuccessHandler))
// 基于 token , 不需要 session
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// 设置 处理鉴权失败、认证失败
.exceptionHandling(
exceptions -> exceptions.authenticationEntryPoint(new MyAuthenticationEntryPoint())
.accessDeniedHandler(new MyAccessDeniedHandler())
)
// 下面开始设置权限
.authorizeHttpRequests(authorizeHttpRequest -> authorizeHttpRequest
// 允许所有 OPTIONS 请求
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
// 允许直接访问 授权登录接口
.requestMatchers(HttpMethod.POST, "/web/authenticate").permitAll()
// 允许 SpringMVC 的默认错误地址匿名访问
.requestMatchers("/error").permitAll()
// 其他所有接口必须有Authority信息,Authority在登录成功后的UserDetailImpl对象中默认设置“ROLE_USER”
//.requestMatchers("/**").hasAnyAuthority("ROLE_USER")
.requestMatchers("/heartBeat/**", "/main/**").permitAll()
// 允许任意请求被已登录用户访问,不检查Authority
.anyRequest().authenticated()
)
// 添加过滤器
.addFilterBefore(myAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class)
.build();
}
@Bean
public UserDetailsService userDetailsService() {
return userDetailsService::loadUserByUsername;
}
/**
* 调用loadUserByUserName获取userDetail信息,在AbstractUserDetailsAuthenticationProvider里执行用户状态检查
*
* @return
*/
@Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService);
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
// @Bean
// public WebSecurityCustomizer webSecurityCustomizer() {
// return (web) -> web.ignoring().requestMatchers();
// }
/**
* 配置跨源访问(CORS)
*
* @return
*/
@Bean
CorsConfigurationSource corsConfigurationSource() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());
return source;
}
}