Spring Security+Oauth2+JWT实现用户登录逻辑,以及使用login接口登录成功返回token获取

目录

pom引入

配置Spring Security

1.实现UserDetailsService接口

2.登录成功处理

3.登录失败处理

4.登出处理

5.没有权限处理设置

6.匿名用户访问处理

7.指定加密方式

8.WebSecurityConfig配置

oauth2处理

配置授权服务器

配置资源服务器

jwt配置

jwt转换器

jwt扩展存储

本文整合了前两篇文章,再结合Oauth2实现了单点登录的基本处理。

之前的文章:

SpringBoot整合Spring Security实现前后端分离登录权限处理_zmgst的博客-CSDN博客

SpringBoot整合Spring Security+JWT实现前后端分离登录权限处理_zmgst的博客-CSDN博客

pom引入

 
        11
        5.1.6.RELEASE
        1.2.46
    
    

        
            org.springframework.boot
            spring-boot-starter-web
        
        
        
            org.springframework.boot
            spring-boot-starter-security
        

        
            org.springframework.security.oauth
            spring-security-oauth2
            2.3.6.RELEASE
        

        
            org.springframework.security
            spring-security-jwt
            1.0.9.RELEASE
        

        
            org.springframework.boot
            spring-boot-starter-jdbc
        
        
            org.mybatis.spring.boot
            mybatis-spring-boot-starter
            2.2.0
        
        
            mysql
            mysql-connector-java
            runtime
        
        
        
            com.alibaba
            druid
            1.1.21
        
        
        
            org.projectlombok
            lombok
        
        
        
            org.springframework.boot
            spring-boot-starter-data-redis
        
        
            org.apache.commons
            commons-pool2
        

        
            com.github.axet
            kaptcha
            0.0.9
        
        
        
            javax.xml.bind
            jaxb-api
            2.3.0
        
        
            com.sun.xml.bind
            jaxb-impl
            2.3.0
        
        
            com.sun.xml.bind
            jaxb-core
            2.3.0
        
        
            javax.activation
            activation
            1.1.1
        
        
        
            com.alibaba
            fastjson
            ${fastjson.version}
        
        
        
            cn.hutool
            hutool-all
            5.3.3
        
        
            org.apache.commons
            commons-lang3
            3.8.1
        

        
            commons-codec
            commons-codec
            1.15
        

        
            org.springframework.boot
            spring-boot-configuration-processor
            true
        
    

配置Spring Security

关于SpringSecruity的基本知识,可以参考这位博主的专栏博客

https://blog.csdn.net/qq_32867467/category_9047805.html?spm=1001.2014.3001.5482

一些统一返回封装,数据库表创建处理我就不再写了,上一篇文章里去粘贴吧,这里只做主要流程的梳理

SpringBoot整合Spring Security实现前后端分离登录权限处理_zmgst的博客-CSDN博客

1.实现UserDetailsService接口

@Component
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private SysUserService sysUserService;
    @Autowired
    private SysPermissionService sysPermissionService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        if(StringUtils.isEmpty(username)){
            throw new UsernameNotFoundException("用户名不能为空");
        }
        /**
         * 通过用户名称获取用户信息
         */
        SysUser user=sysUserService.getUserDetails(username);
        if(user==null){
            throw new UsernameNotFoundException("账号不存在,请联系管理员!");
        }
        if(1!=user.getState()){
            throw new RuntimeException("账户已被锁定,请联系管理员!");
        }
        List grantedAuthorities = new ArrayList<>();
        //获取该用户所拥有的权限
        List sysPermissions = sysPermissionService.selectListPermissionByUser(user.getId());
        // 声明用户授权
        sysPermissions.forEach(sysPermission -> {
            GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(sysPermission.getPermissionCode());
            grantedAuthorities.add(grantedAuthority);
        });
        SecuritySysUser ssu=new SecuritySysUser(user.getAccound(),user.getPassword(),grantedAuthorities);
        ssu.setSysuser(user);
        return ssu;
    }
}
SecuritySysUser为继承了UserDetails的实现类User,代码
@EqualsAndHashCode(callSuper = false)
public class SecuritySysUser extends User {
    /**
     * 用户信息
     */
    private SysUser sysuser;
    /**
     * 权限信息
     */
    private List roleList;
    /**
     * 构造方法
     * @param username
     * @param password
     * @param authorities  用户权限列表
     */
    public SecuritySysUser(String username, String password, Collection authorities) {
        super(username, password, true, true, true, true, authorities);
    }
}

两个请求的sql


2.登录成功处理

@Component
public class CustomizeAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {

        //此处还可以进行一些处理,比如登录成功之后可能需要返回给前台当前用户有哪些菜单权限,
        //进而前台动态的控制菜单的显示等,具体根据自己的业务需求进行扩展

        Map results = new HashMap<>();
        //返回json数据
        JsonResult result = ResultTool.success(results);
        //处理编码方式,防止中文乱码的情况
        httpServletResponse.setContentType("text/json;charset=utf-8");
        //塞到HttpServletResponse中返回给前台
        httpServletResponse.getWriter().write(JSON.toJSONString(result));
    }
}

3.登录失败处理

CaptchaException为自定义异常,不需要的去掉即可

@Component
public class CustomizeAuthenticationFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        //返回json数据
        JsonResult result = null;
        if (e instanceof CaptchaException) {
            //验证码错误
            result = ResultTool.fail(ResultCode.USER_CAPTCHA_ERROR);
        } else if (e instanceof AccountExpiredException) {
            //账号过期
            result = ResultTool.fail(ResultCode.USER_ACCOUNT_EXPIRED);
        } else if (e instanceof BadCredentialsException) {
            //密码错误
            result = ResultTool.fail(ResultCode.USER_CREDENTIALS_ERROR);
        } else if (e instanceof CredentialsExpiredException) {
            //密码过期
            result = ResultTool.fail(ResultCode.USER_CREDENTIALS_EXPIRED);
        } else if (e instanceof DisabledException) {
            //账号不可用
            result = ResultTool.fail(ResultCode.USER_ACCOUNT_DISABLE);
        } else if (e instanceof LockedException) {
            //账号锁定
            result = ResultTool.fail(ResultCode.USER_ACCOUNT_LOCKED);
        } else if (e instanceof InternalAuthenticationServiceException) {
            //用户不存在
            result = ResultTool.fail(ResultCode.USER_ACCOUNT_NOT_EXIST);
        }else{
            //其他错误
            result = ResultTool.fail(ResultCode.COMMON_FAIL);
        }
        //处理编码方式,防止中文乱码的情况
        httpServletResponse.setContentType("text/json;charset=utf-8");
        //塞到HttpServletResponse中返回给前台
        httpServletResponse.getWriter().write(JSON.toJSONString(result));
    }
}

CaptchaException:

public class CaptchaException extends AuthenticationException {

    public CaptchaException(String msg) {
        super(msg);
    }
}

4.登出处理

@Component
public class CustomizeLogoutSuccessHandler implements LogoutSuccessHandler {

    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        if (authentication != null) {
            new SecurityContextLogoutHandler().logout(request, response, authentication);
        }
        JsonResult result = ResultTool.success();
        response.setContentType("text/json;charset=utf-8");
        response.getWriter().write(JSON.toJSONString(result));
    }
}

5.没有权限处理设置

@Component
public class CustomizeAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        JsonResult result = ResultTool.fail(ResultCode.NO_PERMISSION);
        response.setContentType("text/json;charset=utf-8");
        response.getWriter().write(JSON.toJSONString(result));
    }
}

6.匿名用户访问处理

@Component
public class CustomizeAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        JsonResult result = ResultTool.fail(ResultCode.USER_NOT_LOGIN);
        response.setContentType("text/json;charset=utf-8");
        response.getWriter().write(JSON.toJSONString(result));
    }
}

7.指定加密方式

在WebSecurityConfig配置文件添加
    /**
     * 指定加密方式
     * @return
     */
    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

8.WebSecurityConfig配置

这里有些类可能还没有写到,在后面代码里,验证码过滤器的处理,想添加的可以参考之前博客:

SpringBoot整合Spring Security+JWT实现前后端分离登录权限处理_zmgst的博客-CSDN博客

/**
 * spring security 配置类
 * @Author: zm
 * @Description:
 * @Date: 2022/4/22 13:48
 */
@Configuration
@EnableWebSecurity  //开启Spring Security的功能
//prePostEnabled属性决定Spring Security在接口前注解是否可用@PreAuthorize,@PostAuthorize等注解,设置为true,会拦截加了这些注解的接口
@EnableGlobalMethodSecurity(prePostEnabled = true)
//@Order(1)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 自定义用户登录操作
     */
    @Autowired
    private UserDetailsServiceImpl userDetailsService;

    /**
     * 匿名用户访问无权限资源时的异常
     */
    @Autowired
    private CustomizeAuthenticationEntryPoint authenticationEntryPoint;
    /**
     * 登陆失败执行方法
     */
    @Autowired
    private CustomizeAuthenticationFailureHandler authenticationFailureHandler;

    /**
     * 没有权限设置
     */
    @Autowired
    private CustomizeAccessDeniedHandler customizeAccessDeniedHandler;
    /**
     * 登出成功执行方法
     */
    @Autowired
    private CustomizeLogoutSuccessHandler logoutSuccessHandler;

    /**
     * 验证码过滤
     */
  //  @Autowired
 //   private CaptchaFilter captchaFilter;

    /**
     *登录成功执行方法
     * @return
     */
    @Bean
    public CustomizeAuthenticationSuccessHandler loginSuccessHandler() {
        return new CustomizeAuthenticationSuccessHandler();
    }

    /**
     * 指定加密方式
     * @return
     */
    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    /**
     * AuthenticationManager
     * 

* 如果不声明,会导致授权服务器无AuthenticationManager, * 密码模式:而password方式无法获取token */ @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } /** * 用户名密码授权管理器 * * @return DaoAuthenticationProvider */ @Bean public UserNamePasswordAuthenticationProvider daoAuthenticationProvider() { return new UserNamePasswordAuthenticationProvider(userDetailsService, passwordEncoder()); } @Override protected void configure(AuthenticationManagerBuilder auth){ auth.authenticationProvider(daoAuthenticationProvider()); } @Override protected void configure(HttpSecurity http) throws Exception { http.cors().and().csrf().disable(); http .authorizeRequests() .antMatchers(HttpMethod.POST, "/sysUser/addUser").permitAll() // 允许post请求/add-user,而无需认证 .antMatchers("/sysUser/captcha").permitAll()//验证码放过 .antMatchers("/login/**").permitAll()//验证码放过 .antMatchers("/oauth/**").permitAll()//oauth2 请求路径放过 .antMatchers(HttpMethod.OPTIONS).permitAll() .anyRequest().authenticated() // 有请求都需要验证 //登入 .and().formLogin(). permitAll()//允许所有用户 .successHandler(loginSuccessHandler()).//登录成功处理逻辑 failureHandler(authenticationFailureHandler).//登录失败处理逻辑 //登出 and().logout(). permitAll()//允许所有用户 .logoutSuccessHandler(logoutSuccessHandler)//登出成功处理逻辑 //异常处理(权限拒绝、登录失效等) .and() .exceptionHandling() .authenticationEntryPoint(authenticationEntryPoint)//匿名用户访问无权限资源时的异常处理 .accessDeniedHandler(customizeAccessDeniedHandler) // 无状态session,不进行存储 禁用session .and() .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) ; http.addFilterBefore(securityInterceptor,FilterSecurityInterceptor.class); //http.addFilterBefore(captchaFilter, UsernamePasswordAuthenticationFilter.class); } }

UserNamePasswordAuthenticationProvider:
public class UserNamePasswordAuthenticationProvider extends DaoAuthenticationProvider {


    /**
     * 构造方法
     *
     * @param userDetailsService 用户信息服务
     * @param passwordEncoder    密码工具
     */
    public UserNamePasswordAuthenticationProvider(UserDetailsService userDetailsService, PasswordEncoder passwordEncoder) {
        this(userDetailsService, passwordEncoder, null);
    }
    /**
     * 构造方法
     *
     * @param userDetailsService         用户信息服务
     * @param passwordEncoder            密码工具
     * @param userDetailsPasswordService 修改密码服务
     */
    public UserNamePasswordAuthenticationProvider(UserDetailsService userDetailsService, PasswordEncoder passwordEncoder, UserDetailsPasswordService userDetailsPasswordService) {
        super();
        setUserDetailsService(userDetailsService);
        setPasswordEncoder(passwordEncoder);
        setUserDetailsPasswordService(userDetailsPasswordService);
    }

    /**
     * 验证通过后,查询权限等信息
     *
     * @param principal      principal
     * @param authentication authentication
     * @param user           user
     * @return org.springframework.security.core.Authentication
     */
    @Override
    protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) {
        Set authorities;
        SecuritySysUser securityUser = (SecuritySysUser) user;
        return super.createSuccessAuthentication(principal, authentication, securityUser);
    }

}

oauth2处理

Oauth2有4种授权类型,这里只展示一种常用的密码模式的使用

配置授权服务器

@Configuration
@EnableAuthorizationServer
public class Oauth2ServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private UserDetailsServiceImpl userDetailsService;

    @Autowired
    @Qualifier("jwtTokenStore")
    private TokenStore tokenStore;

    @Autowired
    private JwtAccessTokenConverter jwtAccessTokenConverter;

    /**
     * 自定义token处理
     */
    @Autowired
    private JwtTokenEnhancer jwtTokenEnhancer;
    /**
     * 配置认证客户端
     * @param clients
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                // 配置client_id
                .withClient("client")
                // 配置client_secret
                .secret(passwordEncoder.encode("123123"))
                //配置访问token的有效期
                .accessTokenValiditySeconds(36000)
                //配置刷新token的有效期
                .refreshTokenValiditySeconds(864000)
                //配置redirect_uri,用于授权成功后跳转
                .redirectUris("http://www.baidu.com")
                //配置申请的权限范围
                .scopes("all")
                /**
                 * 配置grant_type,表示授权类型
                 *
                 *  authorization_code:授权码模式
                 *  implicit:简化模式
                 *  password:密码模式
                 *  client_credentials: 客户端模式
                 *  refresh_token: 更新令牌
                 */
                .authorizedGrantTypes("password", "refresh_token");
    }

    /**
     * 自定义授权服务配置
     * 使用密码模式需要配置
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        //增加转换链路,以增加自定义属性
        TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
        enhancerChain.setTokenEnhancers(Arrays.asList(jwtTokenEnhancer, jwtAccessTokenConverter));

        endpoints.authenticationManager(authenticationManager)//使用密码模式需要配置
        .userDetailsService(userDetailsService) //刷新令牌授权包含对用户信息的检查
        .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST)//支持GET,POST请求
        .reuseRefreshTokens(false)//refresh_token是否重复使用
        .tokenStore(tokenStore) // 配置存储令牌策略
//        .accessTokenConverter(jwtAccessTokenConverter) // token转化器,我们转为了JWT
        .tokenEnhancer(enhancerChain) ///配置自定义tokenEnhancer
        ; //支持GET,POST请求;
    }

    /**
     * 自定义授权令牌端点的安全约束
     * @param security
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security
                .tokenKeyAccess("permitAll()")
                .checkTokenAccess("permitAll()")
                .allowFormAuthenticationForClients();
    }

}

配置资源服务器

/**
 * @Author: zm
 * @Description: 配置资源服务器
 * @Date: 2022/4/26 13:26
 */
@Configuration
@EnableResourceServer
public class ResourceServiceConfig extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
                .formLogin().and()
                .requestMatcher(requestMatcher())
                .authorizeRequests()
                .antMatchers(HttpMethod.OPTIONS).permitAll()
                .anyRequest().authenticated();

    }

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        super.configure(resources);
    }

}

jwt配置

jwt转换器

/**
 * @Author: zm
 * @Description: jwt 配置文件
 * @Date: 2022/4/27 11:15
 */
@Configuration
public class JwtTokenStoreConfig {

    @Autowired
    private CustomProperties customProperties;

    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter();
        // 配置JWT使用的秘钥
        accessTokenConverter.setSigningKey(customProperties.getPrivateKey());
        return accessTokenConverter;
    }

    @Bean
    public TokenStore jwtTokenStore() {
        return new JwtTokenStore(jwtAccessTokenConverter());
    }
}

jwt扩展存储

/**
 * @Author: zm
 * @Description: jwt token扩展存储
 * @Date: 2022/4/27 11:40
 */
@Component
public class JwtTokenEnhancer implements TokenEnhancer {
    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
        String name=authentication.getName();
        SecuritySysUser userInfo = (SecuritySysUser) authentication.getPrincipal();

        Map info = new HashMap<>();
        info.put("enhance", "enhance info");
        ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(info);
        return accessToken;
    }
}
CustomProperties:
/**
 * @Author: zm
 * @Description: 自定义变量
 * @Date: 2022/4/27 11:11
 */
@Setter
@Getter
@Component
@ConfigurationProperties(prefix = "zm.jwt")
public class CustomProperties {

    private long expire;
    private String secret;
    private String header;
    private String privateKey;
}
#application.yml配置文件
zm:
  jwt:
    header: Authorization
    expire: 604800 # 7天,s为单位
    secret: abcdefghabcdefghabcdefghabcdefgh
    privateKey: 123123

到此处我们的所有配置已经完成了,然后我们再修改之前的登录成功处理器CustomizeAuthenticationSuccessHandler的onAuthenticationSuccess方法,如下:

    @Autowired
    private SysUserService sysUserService;
    @Autowired
    private ClientDetailsService clientDetailsService;

    @Autowired
    private AuthorizationServerTokenServices authorizationServerTokenServices;


public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        //更新用户表上次登录时间、更新人、更新时间等字段
        User userDetails = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        SysUser sysUser = sysUserService.getUserDetails(userDetails.getUsername());
//        sysUser.setLastLoginTime(new Date());
        sysUser.setUpdateDate(LocalDateTime.now());
        sysUser.setUpdateBy(sysUser.getAccound());
        sysUserService.update(sysUser);

        //获取生成得token
        String header=httpServletRequest.getHeader(Constant.AUTHORIZATION);
        if(header==null  || !header.startsWith("Basic ")){
            throw new UnapprovedClientAuthenticationException("请求头中无client信息");
        }
        String[] tokens = this.extractAndDecodeHeader(header, httpServletRequest);
        String clientId = tokens[0];
        String clientSecret = tokens[1];

        // 2. 通过 ClientDetailsService 获取 ClientDetails
        ClientDetails clientDetails = clientDetailsService.loadClientByClientId(clientId);
//        String secret=clientDetails.getClientSecret();
//        clientSecret=passwordEncoder.encode(clientSecret);
        TokenRequest tokenRequest = null;
        // 3. 校验 ClientId和 ClientSecret的正确性
        if (clientDetails == null) {
            throw new UnapprovedClientAuthenticationException("clientId:" + clientId + "对应的信息不存在");
        }
//        else if (passwordEncoder.matches(clientSecret,clientDetails.getClientSecret())) {
//            throw new UnapprovedClientAuthenticationException("clientSecret不正确");
//        }
        else {
            // 4. 通过 TokenRequest构造器生成 TokenRequest
            tokenRequest = new TokenRequest(new HashMap<>(), clientId, clientDetails.getScope(), "custom");
        }
        // 5. 通过 TokenRequest的 createOAuth2Request方法获取 OAuth2Request
        OAuth2Request oAuth2Request = tokenRequest.createOAuth2Request(clientDetails);
        // 6. 通过 Authentication和 OAuth2Request构造出 OAuth2Authentication
        OAuth2Authentication auth2Authentication = new OAuth2Authentication(oAuth2Request, authentication);

        // 7. 通过 AuthorizationServerTokenServices 生成 OAuth2AccessToken
        OAuth2AccessToken token = authorizationServerTokenServices.createAccessToken(auth2Authentication);

        //此处还可以进行一些处理,比如登录成功之后可能需要返回给前台当前用户有哪些菜单权限,
        //进而前台动态的控制菜单的显示等,具体根据自己的业务需求进行扩展

        Map results = new HashMap<>();
        results.put(Constant.AUTHORIZATION,new ObjectMapper().writeValueAsString(token));

        //返回json数据
        JsonResult result = ResultTool.success(results);
        //处理编码方式,防止中文乱码的情况
        httpServletResponse.setContentType("text/json;charset=utf-8");
        //塞到HttpServletResponse中返回给前台
        httpServletResponse.getWriter().write(new ObjectMapper().writeValueAsString(token));
    }


    private String[] extractAndDecodeHeader(String header, HttpServletRequest request) {
        byte[] base64Token = header.substring(6).getBytes(StandardCharsets.UTF_8);

        byte[] decoded;
        try {
            decoded = Base64.getDecoder().decode(base64Token);
        } catch (IllegalArgumentException var7) {
            throw new BadCredentialsException("Failed to decode basic authentication token");
        }

        String token = new String(decoded, StandardCharsets.UTF_8);
        int delim = token.indexOf(":");
        if (delim == -1) {
            throw new BadCredentialsException("Invalid basic authentication token");
        } else {
            return new String[]{token.substring(0, delim), token.substring(delim + 1)};
        }
    }
Constant里的数据:
/**
 * 权限header
 */
public static final String AUTHORIZATION = "Authorization";
/**
 * token开头
 */
public static final String BEARER_TYPE = "Bearer";
/**
 * token开头
 */
public static final String ACCESS_TOKEN = "access_token";

参考博文:

微服务安全Spring Security OAuth2实战_沮丧的南瓜的博客-CSDN博客_微服务springsecurity

Spring Security OAuth2自定义Token获取方式 | MrBird

Spring Security Oauth2 JWT、第三方登录、单点登录讲解,并使用Oauth2.0结合微服务进行单点登录_YxinMiracle的博客-CSDN博客_oauth单点登录第三方接入
Spring Security + OAuth2.0 + JWT 实现单点登录_资深糖分大叔的博客-CSDN博客
 

你可能感兴趣的:(springBoot,spring,security,java,spring,boot)