本文讲述的是springboot集成springSecurity和JWT的实现。
前后端分离目前已成为互联网项目开发的业界标准,其核心思想就是前端(APP、小程序、H5页面等)通过调用后端的API接口,提交及返回JSON数据进行交互。
在前后端分离项目中,首先要解决的就是登录及授权的问题。传统的session认证限制了应用的扩展能力,无状态的JWT认证方法应运而生,该认证机制特别适用于分布式站点的单点登录(SSO)场景。
一,导入SpringSecurity与JWT的相关依赖
<!--Security框架-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- jwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.10.6</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.10.6</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.10.6</version>
</dependency>
二,定义SpringSecurity需要的基础处理类
application-dev.properties 加入jwt配置信息
##jwt
# 令牌key
jwt.header = Authorization
# 令牌前缀
jwt.token-start-with = Bearer
# 使用Base64对该令牌进行编码
jwt.base64-secret = U2FsdGVkX1/3Ox76xzrqllLe1lIgoHycDTgwVYrFQTPhG9V1lQPnLerFS/tmN1PzrQmx5243Nu9/iJf88neqOA==
# 令牌过期时间 此处单位/毫秒
jwt.token-validity-in-seconds = 14400000
创建一个jwt的配置类,并注入Spring,便于程序中调用
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* @ProjectName: git-dev
* @Package: com.lq.pys.base.config
* @ClassName: JwtSecurityProperties
* @Author: xxx
* @Description: JWT配置类
* @Date: 2021/2/18 10:55 上午
*/
@Data
@Configuration
@ConfigurationProperties(prefix = "jwt")
public class JwtSecurityProperties {
/** Request Headers : Authorization */
private String header;
/** 令牌前缀,最后留个空格 Bearer */
private String tokenStartWith;
/** Base64对该令牌进行编码 */
private String base64Secret;
/** 令牌过期时间 此处单位/毫秒 */
private Long tokenValidityInSeconds;
/**返回令牌前缀 */
public String getTokenStartWith() {
return tokenStartWith + " ";
}
}
定义无权限访问类
import com.fasterxml.jackson.databind.ObjectMapper;
import com.lq.pys.base.core.BDic;
import com.lq.pys.base.core.BaseOut;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @ProjectName: git-dev
* @Package: com.lq.pys.base.common
* @ClassName: JwtAccessDeniedHandler
* @Author: xxx
* @Description: jwt无权限访问类
* @Date: 2021/2/18 11:28 上午
*/
@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
// 这个是自定义的返回对象,看各自需求
BaseOut baseOut = new BaseOut();
baseOut.setCode(BDic.FAIL);
baseOut.setMessage("无权限查看此页面,请联系管理员!");
baseOut.setTimestamp(Long.valueOf(System.currentTimeMillis()).toString());
response.setContentType("application/json");
response.setStatus(HttpServletResponse.SC_OK);
try {
ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(response.getOutputStream(), baseOut);
} catch (Exception e) {
throw new ServletException();
}
}
}
定义认证失败处理类
import com.fasterxml.jackson.databind.ObjectMapper;
import com.lq.pys.base.core.BDic;
import com.lq.pys.base.core.BaseOut;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @ProjectName: git-dev
* @Package: com.lq.pys.base.common
* @ClassName: JwtAuthenticationEntryPoint
* @Author: xxx
* @Description: JWT认证失败处理类
* @Date: 2021/2/18 11:31 上午
*/
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
BaseOut baseOut = new BaseOut();
baseOut.setCode(BDic.FAIL);
baseOut.setMessage("无权限查看此页面,请联系管理员");
baseOut.setTimestamp(Long.valueOf(System.currentTimeMillis()).toString());
response.setContentType("application/json");
response.setStatus(HttpServletResponse.SC_OK);
try {
ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(response.getOutputStream(), baseOut);
} catch (Exception e) {
throw new ServletException();
}
}
}
三,构建JWT token工具类
工具类实现创建token与校验token功能
import com.lq.pys.base.config.JwtSecurityProperties;
import com.lq.pys.base.core.UserInfo;
import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Component;
import java.security.Key;
import java.util.*;
import java.util.stream.Collectors;
/**
* @ProjectName: git-dev
* @Package: com.lq.pys.util
* @ClassName: JwtTokenUtils
* @Author: xxx
* @Description: JWT
* @Date: 2021/2/18 11:01 上午
*/
@Slf4j
@Component
public class JwtTokenUtils implements InitializingBean {
private final JwtSecurityProperties jwtSecurityProperties;
private static final String AUTHORITIES_KEY = "auth";
private Key key;
public JwtTokenUtils(JwtSecurityProperties jwtSecurityProperties) {
this.jwtSecurityProperties = jwtSecurityProperties;
}
@Override
public void afterPropertiesSet() {
byte[] keyBytes = Decoders.BASE64.decode(jwtSecurityProperties.getBase64Secret());
this.key = Keys.hmacShaKeyFor(keyBytes);
}
public String createToken (Map<String, Object> claims) {
return Jwts.builder()
.claim(AUTHORITIES_KEY, claims)
.setId(UUID.randomUUID().toString())
.setIssuedAt(new Date())
.setExpiration(new Date((new Date()).getTime() + jwtSecurityProperties.getTokenValidityInSeconds()))
.compressWith(CompressionCodecs.DEFLATE)
.signWith(key, SignatureAlgorithm.HS512)
.compact();
}
public Date getExpirationDateFromToken(String token) {
Date expiration;
try {
final Claims claims = getClaimsFromToken(token);
expiration = claims.getExpiration();
} catch (Exception e) {
expiration = null;
}
return expiration;
}
public Authentication getAuthentication(String token) {
Claims claims = Jwts.parser()
.setSigningKey(key)
.parseClaimsJws(token)
.getBody();
Collection<? extends GrantedAuthority> authorities =
Arrays.stream(claims.get(AUTHORITIES_KEY).toString().split(","))
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
HashMap map =(HashMap) claims.get("auth");
UserInfo principal = new UserInfo(map);
return new UsernamePasswordAuthenticationToken(principal, token, authorities);
}
public boolean validateToken(String authToken) {
try {
Jwts.parser().setSigningKey(key).parseClaimsJws(authToken);
return true;
} catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) {
log.error("token失效",e);
} catch (ExpiredJwtException e) {
log.error("token过期",e);
} catch (UnsupportedJwtException e) {
log.error("无效的token",e);
} catch (IllegalArgumentException e) {
log.error("处理token异常.",e);
}
return false;
}
private Claims getClaimsFromToken(String token) {
Claims claims;
try {
claims = Jwts.parser()
.setSigningKey(key)
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
claims = null;
}
return claims;
}
}
四,实现token验证的过滤器
该类继承OncePerRequestFilter,它能够确保在一次请求中只通过一次filter。该类使用JwtTokenUtils工具类进行token校验。
import com.lq.pys.base.common.SpringContextHolder;
import com.lq.pys.base.config.JwtSecurityProperties;
import com.lq.pys.util.JwtTokenUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @ProjectName: git-dev
* @Package: com.lq.pys.base.filter
* @ClassName: JwtAuthenticationTokenFilter
* @Author: xxx
* @Description: JWT过滤器
* @Date: 2021/2/18 11:07 上午
*/
@Component
@Slf4j
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenUtils jwtTokenUtils;
public JwtAuthenticationTokenFilter(JwtTokenUtils jwtTokenUtils) {
this.jwtTokenUtils = jwtTokenUtils;
}
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
JwtSecurityProperties jwtSecurityProperties = SpringContextHolder.getBean(JwtSecurityProperties.class);
String requestRri = httpServletRequest.getRequestURI();
//获取request token
String token = null;
String bearerToken = httpServletRequest.getHeader(jwtSecurityProperties.getHeader());
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(jwtSecurityProperties.getTokenStartWith())) {
token = bearerToken.substring(jwtSecurityProperties.getTokenStartWith().length());
}
if (StringUtils.hasText(token) && jwtTokenUtils.validateToken(token)) {
Authentication authentication = jwtTokenUtils.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
log.debug("set Authentication to security context for '{}', uri: {}", authentication.getName(), requestRri);
} else {
log.debug("no valid JWT token found, uri: {}", requestRri);
}
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
}
根据SpringBoot官方让重复执行的filter实现一次执行过程的解决方案,参见官网地址:https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#howto-disable-registration-of-a-servlet-or-filter
需在SpringBoot启动类中,加入以下代码:
/**
* @Description:
* @param filter:
* @return: org.springframework.boot.web.servlet.FilterRegistrationBean
* @author: xxx
* @date: 2021/2/18 11:14 上午
*/
@Bean
public FilterRegistrationBean registration(JwtAuthenticationTokenFilter filter) {
FilterRegistrationBean registration = new FilterRegistrationBean<>(filter);
registration.setEnabled(false);
return registration;
}
五,SpringSecurity的关键配置
SpringBoot推荐使用配置类来代替xml配置,该类中涉及了以上几个bean来供security使用
- JwtAccessDeniedHandler :无权限访问
- jwtAuthenticationEntryPoint :认证失败处理
- jwtAuthenticationTokenFilter :token验证的过滤器
import com.lq.pys.base.exception.JwtAccessDeniedHandler;
import com.lq.pys.base.exception.JwtAuthenticationEntryPoint;
import com.lq.pys.base.filter.JwtAuthenticationTokenFilter;
import com.lq.pys.util.JwtTokenUtils;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
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.config.http.SessionCreationPolicy;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/**
* @ProjectName: git-dev
* @Package: com.lq.pys.base.config
* @ClassName: WebSecurityConfig
* @Author: xxx
* @Description: SpringSecurity关键配置
* @Date: 2021/2/18 11:20 上午
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private final JwtAccessDeniedHandler jwtAccessDeniedHandler;
private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
private final JwtTokenUtils jwtTokenUtils;
public WebSecurityConfig(JwtAccessDeniedHandler jwtAccessDeniedHandler, JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint, JwtTokenUtils jwtTokenUtils) {
this.jwtAccessDeniedHandler = jwtAccessDeniedHandler;
this.jwtAuthenticationEntryPoint = jwtAuthenticationEntryPoint;
this.jwtTokenUtils = jwtTokenUtils;
}
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
// 禁用 CSRF
.csrf().disable()
// 授权异常
.exceptionHandling()
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
.accessDeniedHandler(jwtAccessDeniedHandler)
// 不创建会话
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 放行swagger
.antMatchers("/swagger-ui.html").permitAll()
.antMatchers("/swagger-resources/**").permitAll()
.antMatchers("/webjars/**").permitAll()
.antMatchers("/*/api-docs").permitAll()
// 跨域请求会先进行一次options请求 必须放行的OPTIONS请求
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
//允许匿名及登录用户访问
.antMatchers("/api/auth/**", "/error/**").permitAll()
// 不需要token的访问
.antMatchers("/admin/**").permitAll()
.antMatchers("/login/**").permitAll()
.antMatchers("/sms/**").permitAll()
.antMatchers("/membersTypeInfo/**").permitAll()
.antMatchers("/membersClassInfo/**").permitAll()
.antMatchers("/membersRightsInfo/**").permitAll()
.antMatchers("/oss/**").permitAll()
.antMatchers("/storeUser/**").permitAll()
.antMatchers("/imgConfigure/**").permitAll()
.antMatchers("/userMembersInfo/**").permitAll()
// .antMatchers("/sms/**").permitAll()
// 所有请求都需要认证
.anyRequest().authenticated();
// 禁用缓存
httpSecurity.headers().cacheControl();
// 添加JWT filter
httpSecurity.apply(new TokenConfigurer(jwtTokenUtils));
}
public class TokenConfigurer extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
private final JwtTokenUtils jwtTokenUtils;
public TokenConfigurer(JwtTokenUtils jwtTokenUtils){
this.jwtTokenUtils = jwtTokenUtils;
}
@Override
public void configure(HttpSecurity http) {
JwtAuthenticationTokenFilter customFilter = new JwtAuthenticationTokenFilter(jwtTokenUtils);
http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class);
}
}
}
六,编写Controller/Service进行测试
登录后可以使用 LoginUserUtilV3.getLoginUser().getUserOperationId(); 获取用户信息
/**
* @ProjectName: git-dev
* @Package: com.lq.pys.system.controller
* @ClassName: AdminLoginController
* @Author: xxx
* @Description: 后台登录 控制器
* @Date: 2021/2/8 1:23 下午
*/
@RestController
@RequestMapping("/admin/login/")
public class AdminLoginController extends ZyBaseController {
@Autowired
private LoginService loginService;
/**
* @param in:
* @Description: Admin 密码登录
* @return: com.lq.pys.base.core.BaseOut
* @author: xxx
* @date: 2021/2/8 10:18 上午
*/
@PostMapping("password")
public BaseOut password(@RequestBody @Validated(value = {PasswordLogin.class}) LoginIn in) {
in.setLoginType(LoginDic.LOGIN_TYPE.PASSWORD);
LoginAdminUserOut loginAdminUserOut = loginService.adminLogin(in);
return setSuccessBaseOut(loginAdminUserOut);
}
}
/**
* @ProjectName: git-dev
* @Package: com.lq.pys.system.service
* @ClassName: LoginService
* @Author: xxx
* @Description: 登录业务处理
* @Date: 2021/2/8 9:33 上午
*/
@Slf4j
@Service
public class LoginService {
@Autowired
protected SysUserService sysUserService;
@Autowired
protected CaptchaService captchaService;
@Autowired
private JwtTokenUtils jwtTokenUtils;
/**
* @Description: Admin 用户登录
* @param in:
* @return: com.lq.pys.system.login.out.LoginAdminUserOut
* @author: xxx
* @date: 2021/2/8 10:44 上午
*/
public LoginAdminUserOut adminLogin(LoginIn in) {
/** 校验用户名/密码/图片验证码 */
SysUser sysUser = sysUserService.getUserByAccount(in.getAccount());
Optional.ofNullable(sysUser).orElseThrow(()->new BusinessException("用户不存在,不允许登录"));
Optional.ofNullable(sysUser.getPassword()).filter(s->sysUser.getPassword().equals(SecurityUtil.pwdEncrypt(in.getPassword()))).orElseThrow(()->new BusinessException("登录密码错误,请重新输入"));
// captchaService.check(in.getImageCodekey(),in.getImageCode());
LoginAdminUserOut loginAdminUserOut = new LoginAdminUserOut();
/** 设置用户信息 */
AdminUserOut adminUserOut = new AdminUserOut();
BeanUtils.copyProperties(sysUser,adminUserOut);
loginAdminUserOut.setUserInfo(adminUserOut);
/** 设置用户权限 */
/** 设置token */
String token = jwtTokenUtils.createToken(convertToMap(adminUserOut));
loginAdminUserOut.setToken(token);
return loginAdminUserOut;
}
}
使用IDEA Rest Client测试如下:
无token和token失效返回的错误信息:
使用到的用户对象的类:
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.lq.pys.system.dto.sys.ZySysRole;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.util.CollectionUtils;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
@Data
public class LoginUserV3 implements UserDetails {
/**
* 业务ID
*/
private String userOperationId;
/**
* 登录帐户
*/
private String account;
/**
* 手机号
*/
private String phone;
/**
* 密码
*/
private String password;
/**
* 邀请码
*/
private String invitation;
/**
* 创建时间
*/
private Date createTime;
/**
* 是否是章鱼管理员
*/
private Boolean isZyAdmin = false;
/**
* 登陆UUID
*/
private String loginUUID;
private Set<ZySysRole> sysRoles;
private Set<String> permissions;
@JsonIgnore
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> collection = new HashSet<>();
if (!CollectionUtils.isEmpty(sysRoles)) {
sysRoles.parallelStream().forEach(role -> {
if (role.getRole_name().startsWith("ROLE_")) {
collection.add(new SimpleGrantedAuthority(role.getRole_name()));
} else {
collection.add(new SimpleGrantedAuthority("ROLE_" + role.getRole_name()));
}
});
}
if (!CollectionUtils.isEmpty(permissions)) {
for (String per : permissions){
collection.add(new SimpleGrantedAuthority(per));
}
}
return collection;
}
@Override
public String getUsername() {
return getAccount();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
全局获取用户对象的工具类
import com.alibaba.fastjson.JSONObject;
import com.lq.pys.base.core.UserInfo;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
public class LoginUserUtilV3 {
/**
* @Description: 获取登录信息
* @param :
* @return: com.lq.pys.system.login.LoginUserV3
* @author: xxx
* @date: 2021/2/21 19:04 上午
*/
public static LoginUserV3 getLoginUser() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication instanceof UsernamePasswordAuthenticationToken) {
UsernamePasswordAuthenticationToken authenticationToken = (UsernamePasswordAuthenticationToken) authentication;
Object str = authenticationToken.getPrincipal();
UserInfo userInfo = (UserInfo) str;
LoginUserV3 loginUserV3 = JSONObject.parseObject(JSONObject.toJSONString(userInfo), LoginUserV3.class);
return loginUserV3;
}
return null;
}
}
用户实体类:
/**
* @ProjectName: git-dev
* @Package: com.lq.pys.util
* @ClassName: UserInfo
* @Author: xxx
* @Description: 用户信息类
* @Date: 2021/2/18 11:01 上午
*/
@Data
public class UserInfo implements Serializable{
private static final long serialVersionUID = 4768132985889604776L;
/** 用户ID */
private Long id;
/** 用户业务id */
private String userOperationId;
/** 用户账号 */
private String account;
/** 用户密码 */
private String password;
public UserInfo(HashMap map){
this.userOperationId = map.get("operationId").toString();
this.account = map.get("account").toString();
this.password = map.get("password").toString();
}
}
解决跨域问题:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
/**
* @ProjectName: git-dev
* @Package: com.lq.pys.base.config
* @ClassName: CorsConfig
* @Author: xxx
* @Description: 解决跨域问题
* @Date: 2021/2/18 1:39 下午
*/
@Configuration
public class CorsConfig {
private CorsConfiguration buildConfig() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
/**
* 你需要跨域的地址 注意这里的 127.0.0.1 != localhost
* 表示只允许http://localhost:8080地址的访问(重点哦!!!!)
* corsConfiguration.addAllowedOrigin("http://localhost:8080");
*/
//允许所有域名进行跨域调用
corsConfiguration.addAllowedOrigin("*");
//放行全部原始头信息
corsConfiguration.addAllowedHeader("*");
//允许所有请求方法跨域调用
corsConfiguration.addAllowedMethod("*");
//允许跨越发送cookie
corsConfiguration.setAllowCredentials(true);
return corsConfiguration;
}
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
//配置 可以访问的地址
source.registerCorsConfiguration("/**", buildConfig()); // 4
return new CorsFilter(source);
}
麻麻思day.