1.基于OAuth 2.0+JWT+spring security完成认证授权
首先分析一下为什么要用OAuth2和JWT来做
- 单点登录(SSO)方案单击登录方案是最常见的解决方案,但单点登录需要每个与用户交互的服务都必须与认证服务进行通信,这不但会造成重复,也会产生大量琐碎的网络流量;
- 分布式会话(Session)方案通过将用户会话信息存储在共享存储中,如Redis,并使用用户会话的ID作为key来实现分布式哈希映射。当用户访问微服务时,会话数据就可以从共享存储中获取。该解决方案在高可用和扩展方面都很好,但是由于会话信息保存在共享存储中,所以需要一定的保护机制保护数据安全,因此在具体的实现中会具有比较高的复杂度。
- 客户端令牌(Token)方案令牌由客户端生成,并由认证服务器签名。在令牌中会包含足够的信息,客户端在请求时会将令牌附加在请求上,从而为各个微服务提供用户身份数据。此方案解决了分布式会话方案的安全性问题,但如何及时注销用户认证信息则是一个大问题,虽然可以使用短期令牌并频繁地与认证服务器进行校验,但并不可以彻底解决。JWT(JSON Web Tokens)是非常出名的客户端令牌解决方案,它足够简单,并且对各种环境支持程度也比较高
-
客户端令牌与API网关结合
通过在微服务架构中实施API网关,可以将原始的客户端令牌转换为内部会话令牌。一方面可以有效地隐藏微服务,另一方面通过API网关的统一入口可以实现令牌的注销处理。在David Borsos的第二个方案:分布式Session方案中要求开发者能够将用户会话信息单独拎出来进行集中管理。业界比较成熟的开源项目有Spring Session,其使用Redis数据库或缓存机制来实现Session存储,并通过过滤器实现Session数据的自动加载。随着近几年云服务应用的发展,基于令牌(Token)的认证使用范围也越来越广。对于基于令牌认证通常包含下面几层含义:
- 令牌是认证用户信息的集合,而不仅仅是一个无意义的ID。
- 在令牌中已经包含足够多的信息,验证令牌就可以完成用户身份的校验,从而减轻了因为用户验证需要检索数据库的压力,提升了系统性能。
- 因为令牌是需要服务器进行签名发放的,所以如果令牌通过解码认证,我们就可以认为该令牌所包含的信息是合法有效的。
- 服务器会通过HTTP头部中的Authorization获取令牌信息并进行检查,并不需要在服务器端存储任何信息。
- 通过服务器对令牌的检查机制,可以将基于令牌的认证使用在基于浏览器的客户端和移动设备的App或是第三方应用上。
·可以支持跨程序调用。基于Cookie是不允许垮域访问的,而令牌则不存在这个问题。
综上所述,基于令牌的认证由于会包含认证用户的相关信息,因此可以通过验证令牌来完成用户身份的校验,完全不同于之前基于会话的认证。因此,基于令牌的这个优点,像T微信、支付宝、微博及GitHub等,都推出了基于令牌的认证服务,用于访问所开放的API及单点登录。接下来将重点介绍基于令牌认证方案中的OAuth 2.0和JWT
2.两部分:认证服务端(认证及生成token) 、认证资源服务端(访问其他服务内的资源需要校验)
rs.png3.先看看 客户端授权模式(一般采用授权码模式,简单来说就是你要重定向url认证服务器获取授权码(code),在获取访问令牌。
在上面流程图中第一步之后,会重定向到类似于
http://localhost:8080/token/oauth/authorize?client_id=client1&response_type=code&redirect_uri=/token
会返回一个code
在访问
http://localhost:8080/oauth/token?client_id=client1&grant_type=authorization_code&redirect_uri=/token&code=
附加上code值,这时就会返回access_token
还有一个问题:上面url的请求的路径需要保存在数据库中,需要新建一个表,固定的字段
image.pngCREATE TABLE `oauth_client_details` (
`client_id` varchar(255) NOT NULL,
`resource_ids` varchar(255) DEFAULT NULL,
`client_secret` varchar(255) DEFAULT NULL,
`scope` varchar(255) DEFAULT NULL,
`authorized_grant_types` varchar(255) DEFAULT NULL,
`web_server_redirect_uri` varchar(255) DEFAULT NULL,
`authorities` varchar(255) DEFAULT NULL,
`access_token_validity` int(11) DEFAULT NULL,
`refresh_token_validity` int(11) DEFAULT NULL,
`additional_information` varchar(4096) DEFAULT NULL,
`autoapprove` varchar(255) DEFAULT NULL,
PRIMARY KEY (`client_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
详情见如后代码
4.正式代码 (认证服务端)
pom.xml文件
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.3.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
</dependency>
image.png
主要讲解这三个类,他们的的执行顺序:SecurityConfiguration->MyAuthenticationSuccessHandler->AuthorizationServerConfiguration
首先在用户中心登陆操作,获取账号密码,发送HTTP请求
private JSONObject requestToken(String account, String password, String deviceType) {
String result = null;
try {
RestTemplate restTemplate = new RestTemplate();
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
HttpClient httpClient = HttpClientBuilder.create().setRedirectStrategy(new LaxRedirectStrategy()).build();
factory.setHttpClient(httpClient);
restTemplate.setRequestFactory(factory);
MultiValueMap<String, Object> sendMap = new LinkedMultiValueMap<>();
sendMap.add("username", account);
sendMap.add("password", password);
result = RestTemplateUtil.postForEntityFormData(restTemplate, Datas.AUTH_LOGIN_URL, sendMap, deviceType);
logger.info("认证中心返回结果-------》》》》》" + result);
} catch (Exception e) {
logger.error("error", e);
throw new Exception("500", e);
}
return JSON.parseObject(result);
}
方法执行顺序:userDetailsService(会获取到传进来的username)->protected void configure(HttpSecurity http)
->这里会执行config里的myAuthenticationFailureHandler->(这是第二个类)
import javax.transaction.Transactional;
import org.bifu.distributed.auth.constant.AuthContants;
import org.bifu.distributed.auth.dao.UserMapper;
import org.bifu.distributed.auth.domain.User;
import org.bifu.distributed.auth.dto.SecurityUserDTO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private final static Logger logger = LoggerFactory.getLogger(SecurityConfiguration.class);
@Autowired
private UserMapper userMapper;
@Autowired
private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
@Autowired
private MyAuthenticationFailureHandler myAuthenticationFailureHandler;
@Autowired
public void globalUserDetails(AuthenticationManagerBuilder auth) throws Exception {
// 配置用户来源于数据库
// auth.userDetailsService(userDetailsService()).passwordEncoder(new MyPasswordEncoder());
auth.userDetailsService(userDetailsService()).passwordEncoder(new BCryptPasswordEncoder());
// auth.userDetailsService(userDetailsService()).passwordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());
// auth.userDetailsService(userDetailsService()).passwordEncoder(MyPasswordEncoderFactories.createDelegatingPasswordEncoder());
}
/**
* authorizeRequests()配置路径拦截,表明路径访问所对应的权限,角色,认证信息。
* formLogin()对应表单认证相关的配置
* logout()对应了注销相关的配置
* httpBasic()可以配置basic登录
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin().loginProcessingUrl("/auth/login").successHandler(myAuthenticationSuccessHandler)
.failureHandler(myAuthenticationFailureHandler).and().csrf().disable().sessionManagement()
.maximumSessions(1).expiredUrl("/expiredSession");
logger.info("test是否生成token");
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
@Bean
public UserDetailsService userDetailsService() {
return new UserDetailsService() {
@Override
@Transactional(rollbackOn = Exception.class)
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//(注:用户名的name属性必须为username,密码的name属性必须为password。这是security判断用户输入是否正确的标准)
//
logger.info("登录手机号或邮箱:======"+username);
// 查用户
User user = userMapper.selectByPhoneOrEmail(username, username);
if (user == null) {
throw new UsernameNotFoundException(AuthContants.USER_NOT_EXIST);
}
SecurityUserDTO dto = new SecurityUserDTO();
dto.setId(user.getId());
dto.setUsername(username);
dto.setPassword(user.getPassword());
dto.setDisable(user.getDisable());
// 创建securityUserDTO
// SecurityUserDTO securityUserDTO = new SecurityUserDTO(user);
return dto;
}
};
}
}
这是第二个类,这里会进行重定向获取code
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.bifu.distributed.auth.constant.AuthContants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
@Component
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private final static Logger logger = LoggerFactory.getLogger(MyAuthenticationSuccessHandler.class);
@Value(value = "${prefix.auth}")
private String authPrefix; // /token
@Value(value = "${oauth.redirectUrl}")
private String redirectUrl; // /token
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
String deviceType = request.getHeader("deviceType");
logger.info("访问设备-----------》》》" + deviceType);
if (deviceType == null || "".equals(deviceType)) {
deviceType = "browser";
}
// 重定向url到 /token 接口
if ("browser".equals(deviceType)) {
response.sendRedirect("http://localhost:8080:oauth/authorize?client_id=client1&response_type=code&redirect_uri=/token");
} else if ("app".equals(deviceType)) {
response.sendRedirect(http://localhost:8080:oauth/authorize?client_id=client2&response_type=code&redirect_uri=/token);
}
}
}
因为重定向的url是:redirect_uri=/token
5.在这一步之前需要进行服务端生成公钥和密钥,每个客户端使用获取到的公钥到服务器做认证。
生成一个jks
keytool -genkeypair -alias kevin_key -keyalg RSA -keypass 123456 -keystore kevin_key.jks -storepass 123456
导出公钥
keytool -list -rfc --keystore kevin_key.jks | openssl x509 -inform pem -pubkey
保存文本public_key.txt
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxeI6+R6DsGs5RW21Xu1Fur7iPwGjyngN3SCnwPtdR9jTrQ8EIak+gyjpI/g7gIacHIZKMlVFWoEgjQ7+hIQ5FHBrmSR/S81ezCFjYSjBbdrHYQjMRpn4mEWFmQhIyTRhg1Pb5oTUlWx+L3wc45r6JFdMOlgkKBvfo/7lzwGhxeNp10rfoJcnGDhlfZ3PmoIOYmvg7Z8UwszZpYHWf98164m3hMiPyc81iiy/DEE60OVVepyvynfBwg1aGDyA64w63FZ/2dSwfQ/7VQ7WWJb7oVoIy5pyHslWMuQJPpNCxpOgmb19AgC1GojDSL7WAEq+2gQFrb+7k4PyBdsRYzR9DQIDAQAB
-----END PUBLIC KEY-----
认证服务端在Resource保存jks,认证资源服务端保存public_key.txt
image.png
image.png
下面类主要执行的方法是:accessTokenConverter
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
import com.alibaba.fastjson.JSONObject;
import org.bifu.distributed.auth.constant.AuthContants;
import org.bifu.distributed.auth.dto.SecurityUserDTO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory;
/**
* 认证授权服务端
*
*/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
private final static Logger logger = LoggerFactory.getLogger(AuthorizationServerConfiguration.class);
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private DataSource dataSource;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
//开启密码授权类型
endpoints.authenticationManager(this.authenticationManager);
endpoints.accessTokenConverter(accessTokenConverter());
//配置token存储方式
endpoints.tokenStore(tokenStore());
endpoints.reuseRefreshTokens(false);
}
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
oauthServer.tokenKeyAccess("isAnonymous() || hasAuthority('ROLE_TRUSTED_CLIENT')");
oauthServer.checkTokenAccess("hasAuthority('ROLE_TRUSTED_CLIENT')");
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// jdbc方式
clients.withClientDetails(clientDetails());
}
/**
* token converter
*
* @return
*/
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter() {
/***
* 重写增强token方法,用于自定义一些token返回的信息
*/
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken,
OAuth2Authentication authentication) {
SecurityUserDTO securityUserDTO =
(SecurityUserDTO) authentication.getUserAuthentication().getPrincipal();
logger.info("重写增强token方法= {}", JSONObject.toJSONString(securityUserDTO));
final Map<String, Object> additionalInformation = new HashMap<>(16);
additionalInformation.put("userId", securityUserDTO.getId());
((DefaultOAuth2AccessToken) accessToken)
.setAdditionalInformation(additionalInformation);
OAuth2AccessToken enhancedToken = super.enhance(accessToken, authentication);
return enhancedToken;
}
};
// 非对称加密
KeyStoreKeyFactory keyStoreKeyFactory =
new KeyStoreKeyFactory(new ClassPathResource("kevin_key.jks"),
"123456".toCharArray());
accessTokenConverter.setKeyPair(keyStoreKeyFactory.getKeyPair("kevin_key"));
return accessTokenConverter;
}
/**
* 定义clientDetails存储的方式-》Jdbc的方式,注入DataSource
*
* @return
*/
@Bean
public ClientDetailsService clientDetails() {
return new JdbcClientDetailsService(dataSource);
}
/**
* token store
*
* @param
* @return
*/
@Bean
public TokenStore tokenStore() {
TokenStore tokenStore = new JwtTokenStore(accessTokenConverter());
return tokenStore;
}
}
现在重定向到/token这个接口
/**
* 授权,登录
*/
@ResponseBody
@RequestMapping(value = "/token")
public ResultDTO<TokenResultDTO> token(HttpServletRequest request, HttpServletResponse response,
RedirectAttributes attributes) {
TokenResultDTO result = this.userService.token(attributes, request, response);
logger.info("获取到token= {}", JSONObject.toJSONString(result));
return new ResultDTO<TokenResultDTO>("200", "succ", result);
}
//DTO类
@Data
public class TokenResultDTO {
private String access_token;
private String token_type;
private String expires_in;
private String scope;
private String jti;
private String refresh_token;
private String userId;
}
访问的url路径 是固定的
public TokenResultDTO token(RedirectAttributes attributes, HttpServletRequest request,
HttpServletResponse response) {
try {
String code = request.getParameter("code");
if (StringUtils.isEmpty(code)) {
throw new BusinessException(AuthContants.CODE_EXCEPTION);
}
// 发送请求token
String deviceType = "browser";
if (request.getHeader("deviceType") != null && !"".equals(request.getHeader("deviceType"))) {
deviceType = request.getHeader("deviceType");
}
HttpHeaders headers = new HttpHeaders();
HttpEntity<String> entity = new HttpEntity<String>(headers);
TokenResultDTO tokenResultDTO = null;
if ("browser".equals(deviceType)) {
tokenResultDTO = this.browserRestTemplate.postForObject(
" http://localhost/oauth/token?client_id=client1&grant_type=authorization_code&redirect_uri=/token&code=" + code,
entity, TokenResultDTO.class);
} else if ("app".equals(deviceType)) {
tokenResultDTO = this.appRestTemplate.postForObject(
" http://localhost/oauth/token?client_id=client2&grant_type=authorization_code&redirect_uri=/token&code=" + code,
entity, TokenResultDTO.class);
}
return new TokenResultDTO(tokenResultDTO.getAccess_token(), tokenResultDTO.getRefresh_token(),
tokenResultDTO.getUserId(), tokenResultDTO.getExpires_in());
} catch (BusinessException e) {
logger.error("token?");
throw new Exception("500", e.getMessage());
} catch (Exception e) {
logger.error("token?");
throw new Exception("500", e.getMessage());
}
}
到这里就获取到了access_token
log日志:
二 认证资源服务端
image.png这里只讲解ResourceServerConfiguration这个类,其他都是检查异常的
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import org.springframework.security.web.AuthenticationEntryPoint;
/**
* 认证授权资源端
*
* @author rs
*
*/
@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
@Autowired
private MyAccessDeniedHandler myAccessDeniedHandler;
@Autowired
private Auth2ResponseExceptionTranslator auth2ResponseExceptionTranslator;
@Autowired
private SecurityAuthenticationEntryPoint securityAuthenticationEntryPoint;
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.resourceId("client1");
resources.tokenServices(defaultTokenServices());
// 定义异常转换类生效
AuthenticationEntryPoint authenticationEntryPoint = new OAuth2AuthenticationEntryPoint();
((OAuth2AuthenticationEntryPoint) authenticationEntryPoint)
.setExceptionTranslator(this.auth2ResponseExceptionTranslator);
resources.authenticationEntryPoint(authenticationEntryPoint);
}
@Override
public void configure(HttpSecurity http) throws Exception {
// 放行路径在这写
http.csrf().disable().exceptionHandling().authenticationEntryPoint(this.securityAuthenticationEntryPoint)
.accessDeniedHandler(myAccessDeniedHandler).and().authorizeRequests()
.antMatchers("/swagger-resources/**", "/v2/**", "/swagger/**", "/swagger**", "/webjars/**", "/aide/**",
"/backstage/**", "/coin/**", "/talla/**", "/asset/**", "/test/**","/blockchain/borrow/**", "/back/**")
.permitAll().anyRequest().authenticated().and().httpBasic().disable();
// ifream的跨域设置
http.headers().frameOptions().sameOrigin();
}
// ===================================================以下代码与认证服务器一致=========================================
/**
* token存储,这里使用jwt方式存储
*
* @param
* @return
*/
@Bean
public TokenStore tokenStore() {
TokenStore tokenStore = new JwtTokenStore(accessTokenConverter());
return tokenStore;
}
/**
* 创建一个默认的资源服务token
*
* @return
*/
@Bean
public ResourceServerTokenServices defaultTokenServices() {
final DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenEnhancer(accessTokenConverter());
defaultTokenServices.setTokenStore(tokenStore());
return defaultTokenServices;
}
/**
* Token转换器必须与认证服务一致
*
* @return
*/
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter() {
};
Resource resource = new ClassPathResource("public_key.txt");
String publicKey = null;
try {
publicKey = inputStream2String(resource.getInputStream());
} catch (final IOException e) {
throw new RuntimeException(e);
}
accessTokenConverter.setVerifierKey(publicKey);
return accessTokenConverter;
}
// ===================================================以上代码与认证服务器一致=========================================
private String inputStream2String(InputStream is) throws IOException {
BufferedReader in = new BufferedReader(new InputStreamReader(is));
StringBuffer buffer = new StringBuffer();
String line = "";
while ((line = in.readLine()) != null) {
buffer.append(line);
}
return buffer.toString();
}
}
主要讲解public void configure(HttpSecurity http)这个方法:我们在测试的时候往往希望直接就能访问到,不需要token,可以在这里添加放行路径。
个人理解,欢迎指正交流哦!!!,随后会把代码公布在gitee中:https://gitee.com/ran_song
IMG_2423.JPG
网友评论