SpringSecurity总结

图片如果太小可以右键在新标签打开或者按住 ctrl+鼠标滑轮调整页面尺寸调整。

基础

核心

认证与授权

与Shiro联系

SpringSecurity 在 SpringBoot 出现前因为配置复杂使用较少,但是在SpringBoot 出现后搭配使用开发效率大大提高。是一款重量级框架。而 Shiro 是一款轻量级框架,配置简单一些,所以如果不使用 SpringBoot,那么一般搭配 Shiro,而使用SpringBoot 就搭配 SpringSecurity。

核心接口

UserDetailsService

定义了SpringSecurity 查询用户信息的接口方法,在SpringSecurity 认证时,并不是直接通过用户名密码去数据库比对,没有对应就返回,而是先通过 username 去数据库查到对应的用户信息,然后进行拼接成 SpringSecurity 内部维护的用户对象,然后由内部方法进行密码比对。而查询数据库返回用户对象的接口方法就是由 UserDetailsService 接口定义的。

UserDetails  

上面说到数据库查询用户信息会返回一个SpringSecurity 内部维护的用户对象。这个用户抽象类就是 UserDetails,其内部结构如下

public interface UserDetails extends Serializable {
    // ~ Methods
    // ========================================================================================================

    /**
     * Returns the authorities granted to the user. Cannot return <code>null</code>.
     * 授权列表
     * @return the authorities, sorted by natural key (never <code>null</code>)
     */
    Collection<? extends GrantedAuthority> getAuthorities();

    /**
     * Returns the password used to authenticate the user.
     *
     * @return the password
     */
    String getPassword();

    /**
     * Returns the username used to authenticate the user. Cannot return <code>null</code>.
     *
     * @return the username (never <code>null</code>)
     */
    String getUsername();

    /**
     * Indicates whether the user's account has expired. An expired account cannot be
     * authenticated.
     * 是否过期
     * @return <code>true</code> if the user's account is valid (ie non-expired),
     * <code>false</code> if no longer valid (ie expired)
     */
    boolean isAccountNonExpired();

    /**
     * Indicates whether the user is locked or unlocked. A locked user cannot be
     * authenticated.
     * 是否锁定,如果锁定就无法验证
     * @return <code>true</code> if the user is not locked, <code>false</code> otherwise
     */
    boolean isAccountNonLocked();

    /**
     * Indicates whether the user's credentials (password) has expired. Expired
     * credentials prevent authentication.
     * 用户凭证是否过期,过期的凭证会阻止身份验证
     * @return <code>true</code> if the user's credentials are valid (ie non-expired),
     * <code>false</code> if no longer valid (ie expired)
     */
    boolean isCredentialsNonExpired();

    /**
     * Indicates whether the user is enabled or disabled. A disabled user cannot be
     * authenticated.
     * 用户是启用还是禁用,无法对禁用的用户进行身份验证
     * @return <code>true</code> if the user is enabled, <code>false</code> otherwise
     */
    boolean isEnabled();
}

在使用时可以让自定义用户来实现这个接口。

PasswordEncoder

密码接口,一般使用 BCryptPasswordEncoder 来作为默认的密码转换器。SpringSecurity 在加密时引入盐,使得加密过程是不可逆的,而加密后的字符串包含盐信息,在比较方法中会对加密后的密码进行解析,解析出盐值,然后对输入密码进行加密,比较输入密码加密后的结果是否与原密码加密后的结果一致。使用 encode 方法进行加密, matches 方法进行密码比较。如果一致返回 true。

常用配置

用户名密码配置

方式一、配置文件

方式二、配置类

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Resource
    private PasswordEncoder passwordEncoder;


    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        String encode = passwordEncoder.encode("123");
        auth.inMemoryAuthentication().withUser("lucy").password(encode).roles("admin");
//        super.configure(auth);
    }

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

}
View Code

 方式三、自定义配置

 因为一般项目用户名密码都是存在数据库的,所以这是最主流的。

1、配置UserDetails,返回用户信息

@Service("userDetailsService")
public class MyUserDetailService implements UserDetailsService {

    @Resource
    private PasswordEncoder passwordEncoder;
    @Resource
    private UsersMapper usersMapper;

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        Users user = usersMapper.selectOne(new QueryWrapper<Users>().eq("username", s));
        if(user == null){
            throw new UsernameNotFoundException("用户名不存在!");
        }
        // 权限列表,role
        List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("role");
        return new User(s, passwordEncoder.encode(user.getPassword()),auths);
    }

}
View Code

2、添加配置类,将userDetails注册进 SpringSecurity

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Resource
    private UserDetailsService userDetailsService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

}
View Code

记住我

原理

在登陆后会向数据库的 persistent_logins 表中插入一条记录,表结构如下

series 是主键, 随后将 series 和 token 进行算法转换成字符串发给客户端,后面客户端会携带 Cookie ,当下次访问时后端会解析 Cookie ,解析成 series 和 token ,然后去表中匹配,验证token是否一致,以及 last_used + 存活时间是否到期,如果都满足就再以 name 走 UserDetailsService 的方法,返回用户信息。

配置

建表语句:

DROP TABLE IF EXISTS `persistent_logins`;
CREATE TABLE `persistent_logins` (
  `username` varchar(64) NOT NULL,
  `series` varchar(64) NOT NULL,
  `token` varchar(64) NOT NULL,
  `last_used` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`series`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
   @Resource
    private DataSource datasource;

    /**
     * 注入记住我token表的数据源
     * @return
     */
    @Bean
    public PersistentTokenRepository persistentTokenRepository(){
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        jdbcTokenRepository.setDataSource(datasource);
//        jdbcTokenRepository.setCreateTableOnStartup(true);      // 是否自动创建token数据表,如果是第一次可以勾选,后面表存在还开启就会报错
        return jdbcTokenRepository;
    }

可以使用在配置方法中添加 ".rememberMeParameter("rem") " 配置记住我功能的name

注意:

1、这里的 last_used 是拒上次打开浏览器登陆开始计算的,也就是每次打开浏览器访问一次 last_used 都会刷新一次。而浏览器内部访问并不会刷新时间。

2、退出后(退出登陆状态)会清除数据库的token数据记录,再次访问需要重新登录

登陆成功处理器

步骤一:增加组件

方式一、继承实现类

@Slf4j
@Component
public class AuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {


    @Override
    public void onAuthenticationSuccess(HttpServletRequest request,
                                        HttpServletResponse response,
                                        Authentication authentication)
            throws ServletException, IOException {
        User user = (User) authentication.getPrincipal();       // 获取Security 内部维护的user对象
        System.out.println(user.getUsername());     // 用户名:aa
        System.out.println(user.getPassword());     // 密码,由于加密,得到的是null:null
        System.out.println(user.getAuthorities());  // 用户权限列表:[ROLE_AAA, ROLE_sale, admins, manager]
        response.sendRedirect("http://www.baidu.com");
    }
}
View Code

方式二、实现底层接口

public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    private String url;

    public MyAuthenticationSuccessHandler(String url) {
        this.url = url;
    }

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        response.sendRedirect(url);
    }
}
View Code

 步骤二:将组件注册进成功处理器配置中

登陆失败处理器

步骤一:增加组件

方式一、实现接口

 方式二、继承实现类

 步骤二:将组件注册进失败处理器配置中

权限认证失败处理器

1、组件

2、配置

用户退出处理器

1、组件

 2、配置

角色权限

访问一个需要权限或角色的页面需要先登陆,如果登陆后还是不能访问就会返回500.

角色、权限、用户关系

权限与角色是多对多,角色与用户也是多对多。权限指的是对某个表具体的增删改查权限,而角色是一系列权限的集合。比如管理员角色拥有对所有表增删改查的权限,普通用户角色只拥有对所有表查询的权限,而用户 admin 拥有管理员角色,用户 A 拥有普通用户的角色。

定义权限

在config里配置路径所需权限,在UserDetailsService里配置用户所拥有的权限。

1、hasAuthority 是与关系,如果在config里配置了多个权限,如”admin,manager”,那么在UserDetailsService也必须对用户配置两个角色权限才可以访问

2、hasAnyAuthority 是或关系,如果在 config里配置了多个权限,如”admin,manager”,那么在UserDetailsService只需要对用户配置一个权限就可以访问

定义角色

角色在 UserDetailsService 实现类中配置需要加 "ROLE_" 前缀

而hasRole 和hasAnyRole 对应权限里的hasAuthority 和hasAnyAuthority,是与和或的关系。

Access 来定义权限、角色

上面的hasRole、hasAuthority 底层都是使用 access 来实现的,所以我们还可以通过底层的access 方法来主直接定义权限、角色。

那么 config 里配置就是如下:

自定义 Access 校验规则

1、组件

2、配置

基于 IP 来限制

这样的话只能接收来自 127.0.0.1 的请求。

定义角色注解

@Secured单个””里不支持使用,隔开,也就是不支持与关系。如果要配置多个或关系,可以使用{}, 在UserDetailsService里只要配置一个就可以访问。

并且只支持定义角色,不支持定义权限,也就是Secured里必须是ROLE_开头

定义角色、权限注解

可以定义角色、也可以定义权限

如果用户拥有的角色是abc,那么在这里可以配置hasRole(‘abc’),也可以配置hasRole(‘ROLE_abc’),而使用config配置类配置则不可以,会报错。而大小写则和配置类一样会区分

先执行后校验注解

可以用于记录访问日志

对返回和传入数据过滤注解

 

CSRF

CSRF 是为了防止用户在开启记住登陆后,其他非法用户截取到登陆用户的 Cookie ,登陆其他用户进行非法操作。

默认是开启的,开启后用户登录时,系统发放一个CsrfToken值(key是 _csrf,value是token值),用户携带该CsrfToken值与用户名、密码等参数完成登录。系统记录该会话的 CsrfToken 值,之后在用户的任何请求中,都必须带上该CsrfToken值,并由系统进行校验。

配置:

相关依赖:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <!--对Thymeleaf添加Spring Security标签支持-->
        <dependency>
            <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-springsecurity5</artifactId>
        </dependency>

开启 CSRF 时配置类不能配置 loginProcessingUrl 和 defaultSuccessUrl 。会影响登陆跳转逻辑。

其他配置

1、如果配置了登陆的URL(也就是loginProcessingUrl),那么自定义Controller里处理的登陆请求就会用不到,走的是SpringSecurity内部的验证方法。

2、anyRequest()必须配置在所有的antMatches后面,也就是笼统的权限配置必须放在其他权限的最后

3、and()是用于连接多个http配置。     

4、在开发时需要添加@EnableWebSecurity注解,这个注解会自动配置安全认证策略和认证信息。

整合OAuth2

关于 OAuth2 与 JWT 可以移步 浅谈常见的认证机制 。

基础依赖

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency> 

基础配置

因为 OAuth2 涉及到资源服务器和授权服务器,所以除了配置 SpringSecurity ,还需要配置资源服务器和授权服务器。

1、SpringSecurity配置:

@EnableWebSecurity
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/oauth/**", "/login/**", "/logout/**").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin().permitAll()
                .and()
                .csrf().disable();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

}
View Code

2、授权服务器配置:定义 app_id、app_secret,以及重定向地址,授权范围等

 这里直接贴下包含下面整合 redis 存储、JWT、SSO总的配置,根据图片需要进行选择

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Resource
    private PasswordEncoder passwordEncoder;
    @Resource
    private AuthenticationManager authenticationManager;
    @Resource
    private UserService userDetailsService;

    @Resource
    private TokenStore jwtTokenStore;       // 使用jwt存储(因为jwt是无状态的,所以并不会持久化)
//    @Resource
//    private TokenStore redisTokenStore;       // 使用redis存储
    @Resource
    private JwtAccessTokenConverter jwtAccessTokenConverter;
    @Resource
    private JwtTokenEnhancer jwtTokenEnhancer;
    /**
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        // 设置 JWT 增强内容
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        List<TokenEnhancer> delegates = new ArrayList<>();
        delegates.add(jwtTokenEnhancer);
        delegates.add(jwtAccessTokenConverter);
        tokenEnhancerChain.setTokenEnhancers(delegates);

        endpoints.authenticationManager(authenticationManager)
                .userDetailsService(userDetailsService)                 // 密码模式需要配置的
                .tokenStore(jwtTokenStore)
                .tokenEnhancer(tokenEnhancerChain)            // 增加额外数据
                .accessTokenConverter(jwtAccessTokenConverter);     // 使用jwt来代替默认的令牌
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()      // 放入内存
                .withClient("client")       // 客户端ID
                .secret(passwordEncoder.encode("112233"))               // 密钥
                // 重定向地址,这里整合SSO设置为客户端的login页面是因为SpringSecurity默认登陆页面的URL就是login,在客户端通过授权服务器通过后
                // 携带令牌重定向到客户端8081的login页面,自动解析令牌完成登陆。
                .redirectUris("http://www.baidu.com")
                .scopes("all")      // 授权范围
                .autoApprove(true)          // 开启自动授权(不需要进入授权页面手动选择授权)
                .accessTokenValiditySeconds(60)         // 过期时间,单位s
                .refreshTokenValiditySeconds(86400)         // 刷新令牌过期时间
                .authorizedGrantTypes("authorization_code","password","refresh_token");        //授权类型:
                                                                                    // authorization_code:授权码模式
                                                                                    // password:密码模式
                                                                                    // refresh_token:支持刷新令牌
    }


    /**
     * 配置单点登陆
     * @param security
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.tokenKeyAccess("isAuthenticated()");
    }
}
View Code

3、资源服务器配置:定义资源服务器资源权限角色配置。

 4、其他:userDetailsService 配置

@Service("userDetailsService")
public class UserService implements UserDetailsService {

    @Resource
    private PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        // 权限列表,role
        List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admin1");
        return new User(s, passwordEncoder.encode("123456"),auths);
    }
}
View Code

自定义用户实体类 user ,权限属性全部设为 true。

public class User implements UserDetails {

    private String username;
    private String password;
    private List<GrantedAuthority> authorities;

    public User(String username, String password, List<GrantedAuthority> authorities) {
        this.username = username;
        this.password = password;
        this.authorities = authorities;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return username;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}
View Code

资源服务器的资源Controller

同样贴上完整代码,根据图片需要选择

@RestController
@RequestMapping("/user")
public class UserController {

    @RequestMapping("/getCurrentUser")
    public Object getCurrentUser(HttpServletRequest request,Authentication authentication){
        String authorization = request.getHeader("Authorization");
        String token = authorization.substring(authorization.lastIndexOf("bearer") + 7);
        return Jwts.parser()
                .setSigningKey("test_key".getBytes(StandardCharsets.UTF_8))          // 密钥必须和加密所用的一致
                .parseClaimsJws(token)
                .getBody();
//        return authentication.getPrincipal();
    }
}
View Code

授权码模式

在上面的授权服务器配置中,已将授权类型设为 授权码模式,所以直接使用上面的配置。

验证

1、获取授权码

访问 http://localhost:8080/oauth/authorize?response_type=code&client_id=client&redirect_uri=http://www.baidu.com&scope=all ,在登陆成功后(因为走的是上面 userDetailsService 的方法,所以用户名任意,密码123456就通过登陆),会重定向授权服务器配置中配置好的 http://www.baidu.com 。并且携带授权服务器返回的授权码 code。

2、获取授权令牌

接下来就可以再次访问 localhost:8080/oauth/token 携带授权码及其他数据来向授权服务器获取授权令牌。

 3、通过令牌访问资源服务器的资源,访问资源服务器上资源的 URL,并携带授权令牌。

密码模式

密码模式因为是通过密码直接获取授权令牌,所以不需要先获取授权码,同时需要设置自定义的 userDetailsService 实现类,以及 authenticationManager 组件

1、ServurityConfig里增加配置:

 2、授权服务器增加配置:

这样配置是同时支持授权码模式与密码模式

验证 

通过密码获取授权令牌

访问资源服务器的资源则和授权码模式验证一样。

 

整合 redis 将令牌存入 redis

1、引入依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>    

2、注册 redis 的 tokenStore 组件进容器

@Configuration
public class RedisConfig {

    @Resource
    private RedisConnectionFactory redisConnectionFactory;

    @Bean
    public TokenStore redisTokenStore(){
        return new RedisTokenStore(redisConnectionFactory);
    }
}
View Code

3、在授权服务器里注册 tokenStore

4、在配置文件里配置 redis 地址密码等。

使用 JWT 作为令牌

1、增加依赖

        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.0</version>
        </dependency>    

2、从容器中移除 redis 的 tokenStore 组件,同时想容器中加入 jwt 的 tokenStore 组件,并且配置 jwt 的转换器

完整代码,根据图片需要自取

@Configuration
public class JwtTokenStoreConfig {

    @Resource
    private JwtAccessTokenConverter jwtAccessTokenConverter;

    // 保存 token的组件
    @Bean
    public TokenStore jwtTokenStore(){
        return new JwtTokenStore(jwtAccessTokenConverter);
    }

    // Jwt 转换器,用于将jwt转换成 OAuth2的令牌
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter(){
        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
        // 设置jwt密匙
        jwtAccessTokenConverter.setSigningKey("test_key");
        return jwtAccessTokenConverter;
    }

    // 配置Jwt的附加信息
    @Bean
    public JwtTokenEnhancer jwtTokenEnhancer(){
        return new JwtTokenEnhancer();
    }

}
View Code

3、注册进授权服务器

JWT 增加额外信息

1、增加 Jwt 附加信息组件并注册进容器

public class JwtTokenEnhancer implements TokenEnhancer {


    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication) {
        HashMap<String, Object> map = new HashMap<>();
        map.put("enhance", "enhancer info");
        ((DefaultOAuth2AccessToken)oAuth2AccessToken).setAdditionalInformation(map);
        return oAuth2AccessToken;
    }
}
View Code

2、在授权服务器里配置 jwt 附加信息组件

3、验证,修改资源服务器资源返回的信息 

设置过期时间和刷新令牌

在授权服务器里增加配置:

在60s后token令牌(access_token)失效后,可以使用刷新令牌重新获取新的令牌,新的令牌过期时间也是60s。

因为密码模式不支持刷新令牌,所以通过授权码模式使用刷新令牌来获取新的令牌

通过刷新令牌获取令牌

整合SSO(单点登陆)

整合 SSO 后验证的原理就成了下面

 各个服务模块都使用同一个授权服务器,也就是图中的认证中心,在第一次访问模块A时会去跳转到授权服务器进行验证,如果通过,那么就会返回给前端一个 token 令牌,以后在访问A或B时,都会携带这个令牌,而验证时都是通过同一个授权服务器验证,所以都会解析通过,进而访问对应的资源。

1、引入依赖

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.0</version>
        </dependency>

2、新建一个模块,配置 SSO 访问授权服务器的地址

server.port=8081
#防止Cookie冲突,冲突会导致登陆验证不通过
server.servlet.session.cookie.name=OAUTH2-CLIENT-SESSIONID1
#授权服务器地址
oauth2-server-url: http://localhost:8080
#与授权服务器对应的配置
security.oauth2.client.client-id=client
security.oauth2.client.client-secret=112233
#获取授权码地址
security.oauth2.client.user-authorization-uri=${oauth2-server-url}/oauth/authorize
#获取令牌地址
security.oauth2.client.access-token-uri=${oauth2-server-url}/oauth/token
#获取jwt令牌地址
security.oauth2.resource.jwt.key-uri=${oauth2-server-url}/oauth/token_key
View Code

3、增加 SSO 模块的资源

@RestController
@RequestMapping("/user")
public class UserController {

    @RequestMapping("/getCurrentUser")
    public Object getCurrentUser(HttpServletRequest request,Authentication authentication){
        return authentication;
    }
}
View Code

4、主程序开启 OAuth2 自动配置

5、在授权服务器的配置增加配置

随后访问客户端资源 http://localhost:8081/user/getCurrentUser 就会先跳转到 http://localhost:8080/login ,也就是授权服务器进行授权验证,通过后经重定向回到 http://localhost:8081/login ,也就是客户端的登陆页面,并且携带授权服务器提供的jwt令牌,所以会自动解析通过验证,最后再访问客户端的资源

原文地址:https://www.cnblogs.com/mengxinJ/p/14897480.html