学成在线(第16天)

 用户认证需求分析

用户认证需求分析

 用户认证与受权

什么是用户身份认证?
用户身份认证即用户去访问系统资源时系统要求验证用户的身份信息,身份合法方可继续访问。常见的用户身份认
证表现形式有:用户名密码登陆,指纹打卡等方式。
什么是用户受权?
用户认证经过后去访问系统的资源,系统会判断用户是否拥有访问资源的权限,只容许访问有权限的系统资源,没
有权限的资源将没法访问,这个过程叫用户受权。前端

单点登陆需求

单点登陆(Single Sign On),简称为 SSO,是目前比较流行的企业业务整合的解决方案之一。
SSO的定义是在多个应用系统中,用户只须要登陆一次就能够访问全部相互信任的应用系统。
下图是SSO的示意图,用户登陆学成网一次便可访问多个系统。java

 第三方认证

什么是第三方认证(跨平台认证)?
当须要访问第三方系统的资源时须要首先经过第三方系统的认证(例如:微信认证),由第三方系统对用户认证通
过,并受权资源的访问权限。程序员

 用户认证技术方案

单点登陆技术方案

分布式系统要实现单点登陆,一般将认证系统独立抽取出来,而且将用户身份信息存储在单独的存储介质,好比:
MySQL、Redis,考虑性能要求,一般存储在Redis中,以下图:web

单点登陆的特色是:
一、认证系统为独立的系统。
二、各子系统经过Http或其它协议与认证系统通讯,完成用户认证。
三、用户身份信息存储在Redis集群。redis

Java中有不少用户认证的框架均可以实现单点登陆:
一、Apache Shiro.
二、CAS
三、Spring security CAS算法

Oauth2 认证

第三方认证技术方案最主要是解决认证协议的通用标准 问题,由于要实现 跨系统认证,各系统之间要遵循必定的
接口协议。
OAUTH协议为用户资源的受权提供了一个安全的、开放而又简易的标准。同时,任何第三方均可以使用OAUTH认
证服务,任何服务提供商均可以实现自身的OAUTH认证服务,于是OAUTH是开放的。业界提供了OAUTH的多种实
现如PHP、JavaScript,Java,Ruby等各类语言开发包,大大节约了程序员的时间,于是OAUTH是简易的。互联网
不少服务如Open API,不少大公司如Google,Yahoo,Microsoft等都提供了OAUTH认证服务,这些都足以说明
OAUTH标准逐渐成为开放资源受权的标准。spring

下边分析一个Oauth2认证的例子,黑马程序员网站使用微信认证的过程:json

Oauth2包括如下角色:
一、客户端
自己不存储资源,须要经过资源拥有者的受权去请求资源服务器的资源,好比:学成在线Android客户端、学成在
线Web客户端(浏览器端)、微信客户端等。
二、资源拥有者
一般为用户,也能够是应用程序,即该资源的拥有者。
三、受权服务器(也称认证服务器)
用来对资源拥有的身份进行认证、对访问资源进行受权。客户端要想访问资源须要经过认证服务器由资源拥有者授
权后方可访问。
四、资源服务器
存储资源的服务器,好比,学成网用户管理服务器存储了学成网的用户信息,学成网学习服务器存储了学生的学习
信息,微信的资源服务存储了微信的用户信息等。客户端最终访问资源服务器获取资源信息。浏览器

Spring security Oauth2 认证解决方案

本项目采用 Spring security + Oauth2完成用户认证及用户受权,Spring security 是一个强大的和高度可定制的身
份验证和访问控制框架,Spring security 框架集成了Oauth2协议,下图是项目认证架构图:安全

一、用户请求认证服务完成认证。
二、认证服务下发用户身份令牌,拥有身份令牌表示身份合法。
三、用户携带令牌请求资源服务,请求资源服务必先通过网关。
四、网关校验用户身份令牌的合法,不合法表示用户没有登陆,若是合法则放行继续访问。
五、资源服务获取令牌,根据令牌完成受权。
六、资源服务完成受权则响应资源信息。

Spring Security Oauth2 研究

Oauth2 受权码模式

Oauth2有如下受权模式:
受权码模式(Authorization Code) 隐式受权模式(Implicit) 密码模式(Resource Owner Password
Credentials) 客户端模式(Client Credentials)
其中受权码模式和密码模式应用较多,本小节介绍受权码模式。

申请受权码

请求认证服务获取受权码:
Get请求:

localhost:40400/auth/oauth/authorize?
client_id=XcWebApp&response_type=code&scop=app&redirect_uri=http://localhost

参数列表以下:
client_id:客户端id,和受权配置类中设置的客户端id一致。
response_type:受权码模式固定为code
scop:客户端范围,和受权配置类中设置的scop一致。
redirect_uri:跳转uri,当受权码申请成功后会跳转到此地址,并在后边带上code参数(受权码)。

首先跳转到登陆页面:

 输入帐号和密码,点击 Login。

接下来进入受权页面:

点击“赞成”。
接下来返回受权码:
认证服务携带受权码跳转redirect_uri

 申请令牌

拿到受权码后,申请令牌。

Post请求:http://localhost:40400/auth/oauth/token

参数以下:
grant_type:受权类型,填写authorization_code,表示受权码模式
code:受权码,就是刚刚获取的受权码,注意:受权码只使用一次就无效了,须要从新申请。
redirect_uri:申请受权码时的跳转url,必定和申请受权码时用的redirect_uri一致。
此连接须要使用 http Basic认证。
什么是http Basic认证?
http协议定义的一种认证方式,将客户端id和客户端密码按照“客户端ID:客户端密码”的格式拼接,并用base64编
码,放在header中请求服务端

 

 

点击发送:
申请令牌成功:

access_token:访问令牌,携带此令牌访问资源
token_type:有MAC Token与Bearer Token两种类型,两种的校验算法不一样,RFC 6750建议Oauth2采用 Bearer
Token(http://www.rfcreader.com/#rfc6750)。
refresh_token:刷新令牌,使用此令牌能够延长访问令牌的过时时间。
expires_in:过时时间,单位为秒。
scope:范围,与定义的客户端范围一致。

资源服务受权

资源服务拥有要访问的受保护资源,客户端携带令牌访问资源服务,若是令牌合法则可成功访问资源服务中的资
源,以下图:

 上图的业务流程以下:

1 、客户端请求认证服务申请令牌
二、认证服务生成令牌
认证服务采用非对称加密算法,使用私钥生成令牌。
三、客户端携带令牌访问资源服务
客户端在Http header 中添加: Authorization:Bearer 令牌。
四、资源服务请求认证服务校验令牌的有效性
资源服务接收到令牌,使用公钥校验令牌的合法性。
五、令牌有效,资源服务向客户端响应资源信息

JWT 研究

JWT介绍

使用JWT的思路是,用户认证经过会获得一个JWT令牌,JWT令牌中已经包括了用户相关的信息,客户端只须要携带
JWT访问资源服务,资源服务根据事先约定的算法自行完成令牌校验,无需每次都请求认证服务完成受权。
JWT令牌受权过程以下图:

什么是JWT?
JSON Web Token(JWT)是一个开放的行业标准(RFC 7519),它定义了一种简介的、自包含的协议格式,用于
在通讯双方传递json对象,传递的信息通过数字签名能够被验证和信任。JWT可使用HMAC算法或使用RSA的公
钥/私钥对来签名,防止被篡改。

JWT令牌的优势:
一、jwt基于json,很是方便解析。
二、能够在令牌中自定义丰富的内容,易扩展。
三、经过非对称加密算法及数字签名技术,JWT防止篡改,安全性高。
四、资源服务使用JWT可不依赖认证服务便可完成受权。
缺点:
1、JWT令牌较长,占存储空间比较大。

生成私钥和公钥

JWT令牌生成采用非对称加密算法
一、生成密钥证书
下边命令生成密钥证书,采用RSA 算法每一个证书包含公钥和私钥
keytool -genkeypair -alias xckey -keyalg RSA -keypass xuecheng -keystore xc.keystore -storepass
xuechengkeystore
Keytool 是一个java提供的证书管理工具
-alias:密钥的别名
-keyalg:使用的hash算法
-keypass:密钥的访问密码
-keystore:密钥库文件名,xc.keystore保存了生成的证书
-storepass:密钥库的访问密码

查询证书信息:
keytool -list -keystore xc.keystore
删除别名
keytool -delete -alias xckey -keystore xc.keystore

生成jwt令牌

在认证工程建立测试类,测试jwt令牌的生成与验证。

/生成一个jwt令牌
@Test
public void testCreateJwt(){
    //证书文件
    String key_location = "xc.keystore";
    //密钥库密码
    String keystore_password = "xuechengkeystore";
    //访问证书路径
    ClassPathResource resource = new ClassPathResource(key_location);
    //密钥工厂
    KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(resource,
keystore_password.toCharArray());
    //密钥的密码,此密码和别名要匹配
 String keypassword = "xuecheng";
    //密钥别名
    String alias = "xckey";
    //密钥对(密钥和公钥)
    KeyPair keyPair = keyStoreKeyFactory.getKeyPair(alias,keypassword.toCharArray());
    //私钥
    RSAPrivateKey aPrivate = (RSAPrivateKey) keyPair.getPrivate();
    //定义payload信息
    Map<String, Object> tokenMap = new HashMap<>();
    tokenMap.put("id", "123");
    tokenMap.put("name", "mrt");
    tokenMap.put("roles", "r01,r02");
    tokenMap.put("ext", "1");
    //生成jwt令牌
    Jwt jwt = JwtHelper.encode(JSON.toJSONString(tokenMap), new RsaSigner(aPrivate));
    //取出jwt令牌
    String token = jwt.getEncoded();
    System.out.println("token="+token);
}
View Code

验证jwt令牌

//资源服务使用公钥验证jwt的合法性,并对jwt解码
    @Test
    public void testVerify(){
        //jwt令牌
        String token
="eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHQiOiIxIiwicm9sZXMiOiJyMDEscjAyIiwibmFtZSI6Im1ydCIsI
mlkIjoiMTIzIn0.KK7_67N5d1Dthd1PgDHMsbi0UlmjGRcm_XJUUwseJ2eZyJJWoPP2IcEZgAU3tUaaKEHUf9wSRwaDgwhrw
fyIcSHbs8oy3zOQEL8j5AOjzBBs7vnRmB7DbSaQD7eJiQVJOXO1QpdmEFgjhc_IBCVTJCVWgZw60IEW1_Lg5tqaLvCiIl26K
48pJB5f‐le2zgYMzqR1L2LyTFkq39rG57VOqqSCi3dapsZQd4ctq95SJCXgGdrUDWtD52rp5o6_0uq‐
mrbRdRxkrQfsa1j8C5IW2‐T4eUmiN3f9wF9JxUK1__XC1OQkOn‐ZTBCdqwWIygDFbU7sf6KzfHJTm5vfjp6NIA";
        //公钥
        String publickey = "‐‐‐‐‐BEGIN PUBLIC KEY‐‐‐‐‐
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAijyxMdq4S6L1Af1rtB8SjCZHNgsQG8JTfGy55eYvzG0B/E4AudR2
prSRBvF7NYPL47scRCNPgLnvbQczBHbBug6uOr78qnWsYxHlW6Aa5dI5NsmOD4DLtSw8eX0hFyK5Fj6ScYOSFBz9cd1nNTvx
2+oIv0lJDcpQdQhsfgsEr1ntvWterZt/8r7xNN83gHYuZ6TM5MYvjQNBc5qC7Krs9wM7UoQuL+s0X6RlOib7/mcLn/lFLsLD
dYQAZkSDx/6+t+1oHdMarChIPYT1sx9Dwj2j2mvFNDTKKKKAq0cv14Vrhz67Vjmz2yMJePDqUi0JYS2r0iIo7n8vN7s83v5u
OQIDAQAB‐‐‐‐‐END PUBLIC KEY‐‐‐‐‐";
        //校验jwt
        Jwt jwt = JwtHelper.decodeAndVerify(token, new RsaVerifier(publickey));
        //获取jwt原始内容
        String claims = jwt.getClaims();
        //jwt令牌
        String encoded = jwt.getEncoded();
        System.out.println(encoded);
    }

 认证接口开发

需求分析

用户登陆的流程图以下:

执行流程:
一、用户登陆,请求认证服务
二、认证服务认证经过,生成jwt令牌,将jwt令牌及相关信息写入Redis,而且将身份令牌写入cookie
三、用户访问资源页面,带着cookie到网关
四、网关从cookie获取token,并查询Redis校验token,若是token不存在则拒绝访问,不然放行
五、用户退出,请求认证服务,清除redis中的token,而且删除cookie中的token

使用redis存储用户的身份令牌有如下做用:
一、实现用户退出注销功能,服务端清除令牌后,即便客户端请求携带token也是无效的。
二、因为jwt令牌过长,不宜存储在cookie中,因此将jwt令牌存储在redis,由客户端请求服务端获取并在客户端存
储。

Redis 配置

redis 链接配置

在认证服务的application.yml文件中添加以下配置:

spring:
  application:
    name: xc‐service‐ucenter‐auth
  redis:
    host: ${REDIS_HOST:127.0.0.1}
    port: ${REDIS_PORT:6379}
    timeout: 5000 #链接超时 毫秒
    jedis:
      pool:
        maxActive: 3
        maxIdle: 3
        minIdle: 1
        maxWait: ‐1 #链接池最大等行时间 ‐1没有限制

测试

@SpringBootTest
@RunWith(SpringRunner.class)
public class RedisTest {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Test
    public void testRedis(){
        //定义key
        String key = "user_token:9734b68f‐cf5e‐456f‐9bd6‐df578c711390";
        //定义Map
        Map<String,String> mapValue = new HashMap<>();
        mapValue.put("id","101");
        mapValue.put("username","itcast");
        String value = JSON.toJSONString(mapValue);
        //向redis中存储字符串
        stringRedisTemplate.boundValueOps(key).set(value,60, TimeUnit.SECONDS);
        //读取过时时间,已过时返回‐2
        Long expire = stringRedisTemplate.getExpire(key);
        //根据key获取value
        String s = stringRedisTemplate.opsForValue().get(key);
        System.out.println(s);
    }
}

认证服务

认证服务须要实现的功能以下:
一、登陆接口
前端post提交帐号、密码等,用户身份校验经过,生成令牌,并将令牌存储到redis。
将令牌写入cookie。
二、退出接口
校验当前用户的身份为合法而且为已登陆状态。
将令牌从redis删除。
删除cookie中的令牌。

业务流程以下:

 Api接口

@Api(value = "用户认证",description = "用户认证接口")
public interface AuthControllerApi {
    @ApiOperation("登陆")
    public LoginResult login(LoginRequest loginRequest);
    @ApiOperation("退出")
    public ResponseResult logout();
}

配置参数

在application.yml中配置参数

auth:
  tokenValiditySeconds: 1200  #token存储到redis的过时时间
  clientId: XcWebApp
  clientSecret: XcWebApp
  cookieDomain: localhost
  cookieMaxAge: ‐1

申请令牌测试

@SpringBootTest
@RunWith(SpringRunner.class)
public class TestClient {
    @Autowired
    LoadBalancerClient loadBalancerClient;
    @Autowired
    RestTemplate restTemplate;
    @Test
    public void testClient(){
        //采用客户端负载均衡,从eureka获取认证服务的ip 和端口
        ServiceInstance serviceInstance =
loadBalancerClient.choose(XcServiceList.XC_SERVICE_UCENTER_AUTH);
        URI uri = serviceInstance.getUri();
        String authUrl = uri+"/auth/oauth/token";
//URI url, HttpMethod method, HttpEntity<?> requestEntity, Class<T> responseType
        // url就是 申请令牌的url /oauth/token
        //method http的方法类型
        //requestEntity请求内容
        //responseType,将响应的结果生成的类型
        //请求的内容分两部分
        //一、header信息,包括了http basic认证信息
        MultiValueMap<String, String> headers = new LinkedMultiValueMap<String, String>();
        String httpbasic = httpbasic("XcWebApp", "XcWebApp");
        //"Basic WGNXZWJBcHA6WGNXZWJBcHA="
        headers.add("Authorization", httpbasic);
        //二、包括:grant_type、username、passowrd
        MultiValueMap<String, String> body = new LinkedMultiValueMap<String, String>();
        body.add("grant_type","password");
        body.add("username","itcast");
        body.add("password","123");
        HttpEntity<MultiValueMap<String, String>> multiValueMapHttpEntity = new
HttpEntity<MultiValueMap<String, String>>(body, headers);
//指定 restTemplate当遇到400或401响应时候也不要抛出异常,也要正常返回值        
        restTemplate.setErrorHandler(new DefaultResponseErrorHandler(){
            @Override
            public void handleError(ClientHttpResponse response) throws IOException {
                //当响应的值为400或401时候也要正常响应,不要抛出异常
                if(response.getRawStatusCode()!=400 && response.getRawStatusCode()!=401){
                    super.handleError(response);
                }
            }
        });
        //远程调用申请令牌
        ResponseEntity<Map> exchange = restTemplate.exchange(authUrl, HttpMethod.POST,
multiValueMapHttpEntity, Map.class);
        Map body1 = exchange.getBody();
        System.out.println(body1);
    }
    private String httpbasic(String clientId,String clientSecret){
        //将客户端id和客户端密码拼接,按“客户端id:客户端密码”
        String string = clientId+":"+clientSecret;
        //进行base64编码
        byte[] encode = Base64.encode(string.getBytes());
        return "Basic "+new String(encode);
    }
}
View Code

 Service

调用认证服务申请令牌,并将令牌存储到 redis。
一、AuthToken
建立 AuthToken模型类,存储申请的令牌,包括身份令牌、刷新令牌、jwt令牌
身份令牌:用于校验用户是否定证
刷新令牌:jwt令牌快过时时执行刷新令牌
jwt令牌:用于受权

@Data
@ToString
@NoArgsConstructor
public class AuthToken {
    String access_token;//身份token
    String refresh_token;//刷新token
    String jwt_token;//jwt令牌
}

申请令牌的service方法以下:

@Service
public class AuthService {
    private static final Logger LOGGER = LoggerFactory.getLogger(AuthService.class);
    @Value("${auth.tokenValiditySeconds}")
    int tokenValiditySeconds;
    @Autowired
    RestTemplate restTemplate;
    @Autowired
    LoadBalancerClient loadBalancerClient;
    @Autowired
    StringRedisTemplate stringRedisTemplate;
    //认证方法
    public AuthToken login(String username,String password,String clientId,String clientSecret){
        //申请令牌
        AuthToken authToken = applyToken(username,password,clientId, clientSecret);
        if(authToken == null){
            ExceptionCast.cast(AuthCode.AUTH_LOGIN_APPLYTOKEN_FAIL);
        }
        //将 token存储到redis
        String access_token = authToken.getAccess_token();
        String content = JSON.toJSONString(authToken);
        boolean saveTokenResult = saveToken(access_token, content, tokenValiditySeconds);
        if(!saveTokenResult){
 ExceptionCast.cast(AuthCode.AUTH_LOGIN_TOKEN_SAVEFAIL);
        }
        return authToken;
    }
    //存储令牌到redis
    private boolean saveToken(String access_token,String content,long ttl){
        //令牌名称
        String name = "user_token:" + access_token;
        //保存到令牌到redis
        stringRedisTemplate.boundValueOps(name).set(content,ttl, TimeUnit.SECONDS);
        //获取过时时间
        Long expire = stringRedisTemplate.getExpire(name);
        return expire>0;
    }
    //认证方法
    private AuthToken applyToken(String username,String password,String clientId,String
clientSecret){
        //选中认证服务的地址
        ServiceInstance serviceInstance =
loadBalancerClient.choose(XcServiceList.XC_SERVICE_UCENTER_AUTH);
        if (serviceInstance == null) {
            LOGGER.error("choose an auth instance fail");
            ExceptionCast.cast(AuthCode.AUTH_LOGIN_AUTHSERVER_NOTFOUND);
        }
        //获取令牌的url
        String path = serviceInstance.getUri().toString()+"/auth/oauth/token";
        //定义body
        MultiValueMap<String,String> formData = new LinkedMultiValueMap<>();
        //受权方式
        formData.add("grant_type", "password");
        //帐号
        formData.add("username",username);
        //密码
        formData.add("password", password);
        //定义头
        MultiValueMap<String,String> header = new LinkedMultiValueMap<>();
        header.add("Authorization", httpbasic(clientId,clientSecret));
        //指定 restTemplate当遇到400或401响应时候也不要抛出异常,也要正常返回值
        restTemplate.setErrorHandler(new DefaultResponseErrorHandler(){
            @Override
            public void handleError(ClientHttpResponse response) throws IOException {
                //当响应的值为400或401时候也要正常响应,不要抛出异常
                if(response.getRawStatusCode()!=400 && response.getRawStatusCode()!=401){
                    super.handleError(response);
                }
            }
        });
        Map map null;
        try {
            //http请求spring security的申请令牌接口
            ResponseEntity<Map> mapResponseEntity = restTemplate.exchange(path, HttpMethod.POST,new HttpEntity<MultiValueMap<String, String>>(formData, header), Map.class);
            map = mapResponseEntity.getBody();
        } catch (RestClientException e) {
            e.printStackTrace();
            LOGGER.error("request oauth_token_password error: {}",e.getMessage());
            e.printStackTrace();
            ExceptionCast.cast(AuthCode.AUTH_LOGIN_APPLYTOKEN_FAIL);
        }
        if(map == null ||
                map.get("access_token") == null ||
                map.get("refresh_token") == null ||
                map.get("jti") == null){//jti是jwt令牌的惟一标识做为用户身份令牌
            ExceptionCast.cast(AuthCode.AUTH_LOGIN_APPLYTOKEN_FAIL);
        }
        AuthToken authToken new AuthToken();
        //访问令牌(jwt)
        String jwt_token = (String) map.get("access_token");
        //刷新令牌(jwt)
        String refresh_token = (String) map.get("refresh_token");
        //jti,做为用户的身份标识
        String access_token = (String) map.get("jti");
        authToken.setJwt_token(jwt_token);
        authToken.setAccess_token(access_token);
        authToken.setRefresh_token(refresh_token);
        return authToken;
    }
    //获取httpbasic认证串
   private String httpbasic(String clientId,String clientSecret){
        //将客户端id和客户端密码拼接,按“客户端id:客户端密码”
        String string = clientId+":"+clientSecret;
        //进行base64编码
        byte[] encode = Base64.encode(string.getBytes());
        return "Basic "+new String(encode);
    }
}
View Code

Controller

AuthController代码以下:

@RestController
public class AuthController implements AuthControllerApi {
    @Value("${auth.clientId}")
    String clientId;
    @Value("${auth.clientSecret}")
    String clientSecret;
@Value("${auth.cookieDomain}")
    String cookieDomain;
    @Value("${auth.cookieMaxAge}")
    int cookieMaxAge;
    @Value("${auth.tokenValiditySeconds}")
    int tokenValiditySeconds;
    @Autowired
    AuthService authService;
   
    @Override
    @PostMapping("/userlogin")
    public LoginResult login(LoginRequest loginRequest) {
        //校验帐号是否输入
        if(loginRequest == null || StringUtils.isEmpty(loginRequest.getUsername())){
            ExceptionCast.cast(AuthCode.AUTH_USERNAME_NONE);
        }
        //校验密码是否输入
        if(StringUtils.isEmpty(loginRequest.getPassword())){
            ExceptionCast.cast(AuthCode.AUTH_PASSWORD_NONE);
        }
        AuthToken authToken = authService.login(loginRequest.getUsername(),
loginRequest.getPassword(), clientId, clientSecret);
        //将令牌写入cookie
        //访问token
        String access_token = authToken.getAccess_token();
        //将访问令牌存储到cookie
        saveCookie(access_token);
        return new LoginResult(CommonCode.SUCCESS,access_token);
    }
    //将令牌保存到cookie
    private void saveCookie(String token){
        HttpServletResponse response = ((ServletRequestAttributes)
RequestContextHolder.getRequestAttributes()).getResponse();
        //添加cookie 认证令牌,最后一个参数设置为false,表示容许浏览器获取
        CookieUtil.addCookie(response, cookieDomain, "/", "uid", token, cookieMaxAge, false);
    }
   
    @Override
    @PostMapping("/userlogout")
    public ResponseResult logout() {
        return null;
    }
}
View Code

 登陆url放行

认证服务默认都要校验用户的身份信息,这里须要将登陆url放行。

在 WebSecurityConfig类中重写 configure(WebSecurity web)方法,以下:

@Override    
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/userlogin");
    }

测试认证接口

使用postman测试:
Post请求:http://localhost:40400/auth/userlogin

 

 

相关文章
相关标签/搜索