SpringSecurity6实现动态权限,rememberMe、OAuth2.0授权登录,退出登录等功能

本文章对应视频可在B站查看SpringSecurity6对应视频教程,记得三连哦,这对我很重要呢!
温馨提示:视频与文章相辅相成,结合学习效果更强哦!

系列文章链接

1、初识SpringSecurity,认识主流Java权限框架,SpringSecurity入门使用
2、SpringSecurity集成数据库,完成认证授权操作
3、SpringSecurity实现动态权限,OAuth2.0授权登录等

动态权限控制

SpringSecurity中我们可以使用基于注解的方式实现,可以控制方法级别的权限认证,有一个问题在于项目中的接口权限如果发生变化,此时代码就要修改,修改之后要重新上线。基于此我们希望可以通过修改配置的方式,去控制接口的权限,实现动态控制。此处我们是通过数据库配置的方式修改【此处也可以将数据缓存到Redis中,因为接口权限不会经常变化】,在SpringSecurity6中实现动态权限控制:

  • SpringSecurity配置文件中再其它请求的配置后边,添加access配置,传入一个【AuthorizationManager】
  • 自定义类实现 AuthorizationManager 接口

实现思路

在实现类中查询当前访问的接口所需要的权限,并且根据当前认证的用户所拥有的权限,判断是否包含接口所需权限,所有数据库中应该有数据记录接口的访问权限

**访问路径:**其实是一个url,可以对url进行权限设置。比如sys/employee/list他的作用是访问员工列表,他需要sys:employee:list【这个权限也是存到数据库中】

修改数据表

CREATE TABLE `ums_menu` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
  `parent_id` bigint NOT NULL DEFAULT '0' COMMENT '父id',
  `menu_name` varchar(255) DEFAULT NULL COMMENT '菜单名',
  `path` varchar(255) DEFAULT NULL COMMENT '访问路径',
  `sort` int DEFAULT '0' COMMENT '排序',
  `perms` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '权限标识',
  `menu_type` int DEFAULT NULL COMMENT '类型:0,目录,1菜单,2:按钮',
  `icon` varchar(255) DEFAULT NULL COMMENT '图标',
  `deleted` int DEFAULT NULL COMMENT '是否删除',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_time` datetime DEFAULT NULL COMMENT '修改时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

实现AuthorizationManager 接口

在该接口中,获取访问路径【URI】,再获取用户的权限列表,做判断就行了,写完之后需要配置到配置类中

package com.stt.springsecuritydemo7.web.manager;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.stt.springsecuritydemo7.domain.entity.UmsMenu;
import com.stt.springsecuritydemo7.mapper.UmsMenuMapper;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.access.intercept.RequestAuthorizationContext;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.function.Supplier;

/**
 * 判断请求路径是否有权限访问
 */
@Component
@Slf4j
public class SttAuthorizationManager implements AuthorizationManager<RequestAuthorizationContext> {

    private final UmsMenuMapper menuMapper;

    public SttAuthorizationManager(UmsMenuMapper menuMapper) {
        this.menuMapper = menuMapper;
    }

    @Override
    public AuthorizationDecision check(Supplier<Authentication> authentication, RequestAuthorizationContext requestAuthorizationContext) {
        // 获取请求路径,获取HttpServletRequest
        HttpServletRequest request = requestAuthorizationContext.getRequest();
        String uri = request.getRequestURI();
        String url = request.getRequestURL().toString();
        // 有些请求不需要认证
        if("/auth/login".equals(uri) || "/logout".equals(uri) || "/error".equals(uri)) {
            return new AuthorizationDecision(true);
        }
        // 根据uri获取路径的权限
        UmsMenu umsMenu = menuMapper.selectOne(new LambdaQueryWrapper<UmsMenu>().eq(UmsMenu::getPath, uri.replaceFirst("/","")));
        if(umsMenu == null) {
            return new AuthorizationDecision(false);
        }
        // 获取路径访问权限
        String menuPerm = umsMenu.getPerms();
        if(menuPerm == null || menuPerm.trim().equals("")) {
            return new AuthorizationDecision(true);
        }

        // 与用户权限集合做判断
        Collection<? extends GrantedAuthority> authorities = authentication.get().getAuthorities();
        for (GrantedAuthority authority : authorities) {
            String userPerm = authority.getAuthority();
            if(userPerm.equals(menuPerm)) {
                return new AuthorizationDecision(true);
            }
        }
        return new AuthorizationDecision(false);
    }
}

动态权限问题

  1. 请求时每次都需要查询数据库菜单的权限,会对数据库造成压力。可以通过redis来缓解数据库压力,引入redis之后需要解决redis与mysql的数据一致性问题
  2. 用户的权限是在登陆时就获取到的,后续的操作并不会获取最新的权限,此时也是可以通过redis来存储用户信息【包含权限信息】,修改权限之后需要修改:
    1. redis中缓存的权限数据
    2. 用户的权限数据
      1. 用户很多,修改繁琐
      2. 用户量不大,改了也就改了

权限这种数据一般很少修改

rememberMe功能

rememberMe也就是记住我功能,在登陆时可以勾选此选项,客户端会记住我们的登录信息,下次访问时可以根据记住的登录信息,实现自动登录。

QQ邮箱

在这里插入图片描述

163邮箱
在这里插入图片描述

gitee
在这里插入图片描述

记住我或持久登录身份验证是指网站能够在会话之间记住主体的身份。这通常是通过向浏览器发送cookie来实现的,cookie在后续的会话中被检测到,并导致自动登录。Spring Security为这些操作提供了两个具体的实现。

  • 使用哈希编码方式保持基于cookie的令牌的安全性
  • 使用数据库或其他持久存储机制来存储生成的令牌。

请注意,这两个实现都需要UserDetailsService。

简单哈希方法

这种方法使用散列来实现有用的记住我的策略。本质上,一个cookie在成功的交互验证后被发送到浏览器,cookie的组成如下:

base64(username + ":" + expirationTime + ":" + algorithmName + ":"
algorithmHex(username + ":" + expirationTime + ":" password + ":" + key))

username: 可由UserDetailsService识别
password: 与检索到的UserDetails中的匹配
expirationTime: “记住我”令牌过期的日期和时间,以毫秒表示
key: 防止修改记住我令牌的私钥
algorithmName:用于生成和验证记住我的令牌签名的算法

记住我令牌仅在指定的时间段内有效,并且仅在用户名、密码和密钥未更改的情况下有效。值得注意的是,这有一个潜在的安全问题,因为捕获的记住我令牌在令牌到期之前可以从任何用户代理使用。如果主体知道令牌被捕获,他们可以很容易地更改密码,并立即使所有有问题的记住我的令牌无效。如果需要更重要的安全性,应该使用持久化令牌。或者,根本不应该使用“记住我”服务

简单的哈希方式只需要在配置类中开启rememberMe功能即可

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    // 关闭csrf
    http.csrf(csrf -> csrf.disable());
    // 配置请求拦截策略
    http.authorizeHttpRequests(auth -> auth.anyRequest().authenticated());
    // 使用SpringSecurity默认的登录页面
    http.formLogin(Customizer.withDefaults());
    // 开启默认的记住我功能,rememberMeCookieName:设置浏览器断存储的cookeie名称,默认为remember-me
    http.rememberMe(remember -> remember.rememberMeCookieName("rememberMe"));
    return http.build();
}

此方式可以将cookie获取到之后放到另一个客户端照样可以访问,而且不会修改cookie值,很不安全,如果需要使用记住我功能可以通过持久化令牌方式会安全一点,但不能保障完全安全哦!

持久化令牌

持久化令牌就是将令牌存储在数据库中,数据库应包含一个persistent_logins表,该表通过使用以下SQL(或等效SQL)创建:

create table persistent_logins (
    series varchar(64) primary key,
    username varchar(64) not null,
    token varchar(64) not null,
    last_used timestamp not null
)

实体类

@Data
@TableName("persistent_logins")
public class PersistentLogins {
    private String series;
    private String username;
    private String token;
    private Date lastUsed;
}

实现类

自定义实现类对数据表进行操作,其实就是在前端登陆时如果选择了记住我功能,则生成token,存到数据表中,退出登录需要将token删除掉。

退出客户端之后再来访问时,如果上一次选择了记住我则自动登录。还会更新token。如果cookie被盗取,则会修改cookie的值,大家可以自行测试一下哦,印象会更深刻

其实就是在实现类中实现token的,增删改查方法。

package com.stt.springsecuritydemo8.token;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.stt.springsecuritydemo8.domain.emtity.PersistentLogins;
import com.stt.springsecuritydemo8.mapper.PersistentLoginsMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.web.authentication.rememberme.PersistentRememberMeToken;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.stereotype.Component;
import java.util.Date;

@Component
@Slf4j
public class DaoCaoPersistentTokenRepositoryImpl implements PersistentTokenRepository {

    private final PersistentLoginsMapper persistentLoginsMapper;

    public DaoCaoPersistentTokenRepositoryImpl(PersistentLoginsMapper persistentLoginsMapper) {
        this.persistentLoginsMapper = persistentLoginsMapper;
    }

    /**
     * 创建token
     * @param token
     */
    @Override
    public void createNewToken(PersistentRememberMeToken token) {
        PersistentLogins persistentLogins = new PersistentLogins();
        persistentLogins.setSeries(token.getSeries());
        persistentLogins.setUsername(token.getUsername());
        persistentLogins.setToken(token.getTokenValue());
        persistentLogins.setLastUsed(token.getDate());
        // 存储到数据库中
        persistentLoginsMapper.insert(persistentLogins);
    }

    @Override
    public void updateToken(String series, String tokenValue, Date lastUsed) {
        LambdaUpdateWrapper<PersistentLogins> updateWrapper = new LambdaUpdateWrapper<>();
        updateWrapper.set(PersistentLogins::getToken,tokenValue).set(PersistentLogins::getLastUsed,lastUsed)
                .eq(PersistentLogins::getSeries,series);
        // 调用修改方法
        persistentLoginsMapper.update(null,updateWrapper);
    }

    /**
     * 获取token
     * @param seriesId
     * @return
     */
    @Override
    public PersistentRememberMeToken getTokenForSeries(String seriesId) {
        LambdaQueryWrapper<PersistentLogins> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(PersistentLogins::getSeries,seriesId);
        // 调用查询方法
        PersistentLogins result = persistentLoginsMapper.selectOne(queryWrapper);

        PersistentRememberMeToken retrunResult = new PersistentRememberMeToken(
                result.getUsername(),result.getSeries(),result.getToken(),result.getLastUsed()
        );
        return retrunResult;
    }

    @Override
    public void removeUserTokens(String username) {
        LambdaUpdateWrapper<PersistentLogins> updateWrapper = new LambdaUpdateWrapper<>();
        updateWrapper.eq(PersistentLogins::getUsername,username);
        persistentLoginsMapper.delete(updateWrapper);
    }
}

配置

将实现类配置到rememberMe功能上

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Autowired
    private DaoCaoPersistentTokenRepositoryImpl persistentTokenRepository;
    // 配置,启用rememberMe功能
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        // 关闭csrf
        http.csrf(csrf -> csrf.disable());
        // 配置请求拦截策略
        http.authorizeHttpRequests(auth -> auth.anyRequest().authenticated());
        // 使用SpringSecurity默认的登录页面
        http.formLogin(Customizer.withDefaults());
        // 开启默认的记住我功能
        http.rememberMe(remember -> remember.rememberMeCookieName("rememberMe")
                        .tokenRepository(persistentTokenRepository));
        return http.build();
    } );
    }
}

实现原理

RememberMeAuthenticationFilter

当启用了记住我功能之后,会自动启用RememberMeAuthenticationFilter这个过滤器,该过滤器的doFilter方法如下

private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
    throws IOException, ServletException {
    // 从SecurityContext中获取Authentication,如果有则认为已经认证过了,直接放行
    // 如果没有 Authentication 则认为没有认证,会去做自动登录【认证就是登录】
    if (this.securityContextHolderStrategy.getContext().getAuthentication() != null) {
        this.logger.debug(LogMessage
                          .of(() -> "SecurityContextHolder not populated with remember-me token, as it already contained: '"
                              + this.securityContextHolderStrategy.getContext().getAuthentication() + "'"));
        chain.doFilter(request, response);
        return;
    }
    // 此处调用 rememberMeServices 的autoLogin方法做自动登录,登录成功则返回一个 Authentication对象
    // 对象名为 rememberMeAuth,此处其实就是根据request中的cookie做解码操作判断cookie值是否正确
    // 后边会详细介绍该方法,该方法在 RememberMeServices类中
    Authentication rememberMeAuth = this.rememberMeServices.autoLogin(request, response);
    // 判断不是null,则认为该cookie存在
    if (rememberMeAuth != null) {
        // Attempt authentication via AuthenticationManager
        try {
            // 此处调用 authenticate方法认证用户
            rememberMeAuth = this.authenticationManager.authenticate(rememberMeAuth);
            // 获取 SecurityContext 对象
            SecurityContext context = this.securityContextHolderStrategy.createEmptyContext();
            // 将 Authentication 放入 SecurityContext 中,大家应该还记得两者的关系吧
            context.setAuthentication(rememberMeAuth);
            this.securityContextHolderStrategy.setContext(context);
            onSuccessfulAuthentication(request, response, rememberMeAuth);
            this.logger.debug(LogMessage.of(() -> "SecurityContextHolder populated with remember-me token: '"
                                            + this.securityContextHolderStrategy.getContext().getAuthentication() + "'"));
            this.securityContextRepository.saveContext(context, request, response);
            if (this.eventPublisher != null) {
                this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
                    this.securityContextHolderStrategy.getContext().getAuthentication(), this.getClass()));
            }
            if (this.successHandler != null) {
                this.successHandler.onAuthenticationSuccess(request, response, rememberMeAuth);
                return;
            }
        }
        // 如果认证过程中出现异常
        catch (AuthenticationException ex) {
            this.logger.debug(LogMessage
                              .format("SecurityContextHolder not populated with remember-me token, as AuthenticationManager "
                                      + "rejected Authentication returned by RememberMeServices: '%s'; "
                                      + "invalidating remember-me token", rememberMeAuth),
                              ex);
            // 调用 rememberMeServices中的 登陆失败方法
            this.rememberMeServices.loginFail(request, response);
            onUnsuccessfulAuthentication(request, response, ex);
        }
    }
    chain.doFilter(request, response);
}

RememberMeServices

通过上边的代码看出,获取cookie是通过RememberMeServices的autoLogin方法实现,登录失败的话要调用loginFail方法,其实它是一个接口,里边主要包含三个方法,这三个方法都有对应的类做了一些实现,如果想自定义的话,就是实现这个接口或者继承内部默认的实现类AbstractRememberMeServices就可以了
在这里插入图片描述

autoLogin

此处的autoLogin代码就是AbstractRememberMeServices这个抽象类的默认实现:

public Authentication autoLogin(HttpServletRequest request, HttpServletResponse response) {
    // 调用获取cookie的方法,该方法的源码放到下边了,返回的值就是rememberMe的值
    String rememberMeCookie = extractRememberMeCookie(request);
    if (rememberMeCookie == null) {
        return null;
    }
    this.logger.debug("Remember-me cookie detected");
    if (rememberMeCookie.length() == 0) {
        this.logger.debug("Cookie was empty");
        cancelCookie(request, response);
        return null;
    }
    // 如果不是null,长度业大于0
    try {
        // 解析Cookie,该方法实现也放在下边了,
        String[] cookieTokens = decodeCookie(rememberMeCookie);
        // 此方法就会去调用loadUserByUsername方法实现认证了。
        // 此处会根据你是基于哈希,还是持久化令牌方法去掉用不同的 RememberMeServices的方法
        // 1、TokenBasedRememberMeServices:哈希方式
        // 2、PersistentTokenBasedRememberMeServices:持久化令牌方式
        UserDetails user = processAutoLoginCookie(cookieTokens, request, response);
        this.userDetailsChecker.check(user);
        this.logger.debug("Remember-me cookie accepted");
        return createSuccessfulAuthentication(request, user);
    }
    catch (CookieTheftException ex) {
        cancelCookie(request, response);
        throw ex;
    }
    catch (UsernameNotFoundException ex) {
        this.logger.debug("Remember-me login was valid but corresponding user not found.", ex);
    }
    catch (InvalidCookieException ex) {
        this.logger.debug("Invalid remember-me cookie: " + ex.getMessage());
    }
    catch (AccountStatusException ex) {
        this.logger.debug("Invalid UserDetails: " + ex.getMessage());
    }
    catch (RememberMeAuthenticationException ex) {
        this.logger.debug(ex.getMessage());
    }
    // 清除cookie
    cancelCookie(request, response);
    return null;
}

// 获取cookie值
protected String extractRememberMeCookie(HttpServletRequest request) {
    Cookie[] cookies = request.getCookies();
    if ((cookies == null) || (cookies.length == 0)) {
        return null;
    }
    // 循环遍历cookie,获取名称符合条件的,也就是rememberMe
    for (Cookie cookie : cookies) {
        if (this.cookieName.equals(cookie.getName())) {
            return cookie.getValue();
        }
    }
    return null;
}
// 解析cookie,返回的是String[]数组,里边包含了cookie的值,编码方式,过期时间等四个信息
protected String[] decodeCookie(String cookieValue) throws InvalidCookieException {
    for (int j = 0; j < cookieValue.length() % 4; j++) {
        cookieValue = cookieValue + "=";
    }
    String cookieAsPlainText;
    try {
        cookieAsPlainText = new String(Base64.getDecoder().decode(cookieValue.getBytes()));
    }
    catch (IllegalArgumentException ex) {
        throw new InvalidCookieException("Cookie token was not Base64 encoded; value was '" + cookieValue + "'");
    }
    String[] tokens = StringUtils.delimitedListToStringArray(cookieAsPlainText, DELIMITER);
    for (int i = 0; i < tokens.length; i++) {
        try {
            tokens[i] = URLDecoder.decode(tokens[i], StandardCharsets.UTF_8.toString());
        }
        catch (UnsupportedEncodingException ex) {
            this.logger.error(ex.getMessage(), ex);
        }
    }
    return tokens;
}

TokenBasedRememberMeServices

如果是传统哈希方式实现的rememberMe就会调用TokenBasedRememberMeServicesprocessAutoLoginCookie方法实现自动登录

protected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request,
			HttpServletResponse response) {
    if (!isValidCookieTokensLength(cookieTokens)) {
        throw new InvalidCookieException(
            "Cookie token did not contain 3 or 4 tokens, but contained '" + Arrays.asList(cookieTokens) + "'");
    }
    // 获取过期时间
    long tokenExpiryTime = getTokenExpiryTime(cookieTokens);
    if (isTokenExpired(tokenExpiryTime)) {
        throw new InvalidCookieException("Cookie token[1] has expired (expired on '" + new Date(tokenExpiryTime)
                                         + "'; current time is '" + new Date() + "')");
    }
    // 调用 UserDetailsService的 loadUserByUsername方法
    UserDetails userDetails = getUserDetailsService().loadUserByUsername(cookieTokens[0]);
    Assert.notNull(userDetails, () -> "UserDetailsService " + getUserDetailsService()
                   + " returned null for username " + cookieTokens[0] + ". " + "This is an interface contract violation");
    // 获取签名方式
    String actualTokenSignature = cookieTokens[2];
    RememberMeTokenAlgorithm actualAlgorithm = this.matchingAlgorithm;
    // If the cookie value contains the algorithm, we use that algorithm to check the
    // signature
    if (cookieTokens.length == 4) {
        actualTokenSignature = cookieTokens[3];
        actualAlgorithm = RememberMeTokenAlgorithm.valueOf(cookieTokens[2]);
    }
    // 根据数字签名计算cookie
    String expectedTokenSignature = makeTokenSignature(tokenExpiryTime, userDetails.getUsername(),
                                                       userDetails.getPassword(), actualAlgorithm);
    if (!equals(expectedTokenSignature, actualTokenSignature)) {
        throw new InvalidCookieException("Cookie contained signature '" + actualTokenSignature + "' but expected '"
                                         + expectedTokenSignature + "'");
    }
    return userDetails;
}

总的来说,RememberMe功能实现是通过上述三个类,可以自己debug一下,印象更深哦!

总结

  1. RememberMe 实质就是将令牌以 Cookie 的形式响应给浏览器,然后浏览器进行本地存储,等下次未登录时,再次再访问系统时,即会拿该令牌连请求一起到服务器端,令牌会使得后台进行自动登录(即用户认证)。
  2. 持久化token方式在关闭浏览器再次访问时存储的cookie值会发生变化,而哈希方式并不会。
  3. 当我们自定义 SecurityFilterChain 安全过滤器链的时候,如果配置了 rememberMe() 的话,那RememberMeAuthenticationFilter 即会是过滤器链中的一员。
  4. 阐述一下流程(实现该功能最主要的类是 RememberMeServices,首次认证的响应和自动登录的认证它都是主角):在没有自定义过滤器的情况下,一般都是使用 UsernamePasswordAuthenticationFilter 进行用户认证的,当点击了 checkbox 按钮连同用户信息一起向服务器端请求时,首先是 UsernamePasswordAuthenticationFilter 去完成认证,如果认证成功会调用 RememberMeServices 中的 onLoginSuccess 方法(这个看具体实现),此时响应中就已存在其令牌信息了(即 Cookie)。当关闭浏览器或Session过期,访问服务器端需要认证的资源时,请求报文中会带着这个Cookie一起到服务器端,会进入到 RememberMeAuthenticationFilter 过滤器中,它会调用 RememberMeServices 中的 autoLogin 方法进行自动登录,在 autoLogin 自动登录过程中,会调用 processAutoLoginCookie 方法进行令牌认证(该方法取决于 RememberMeServices 的实现类),autoLogin 方法执行完成认证成功后,返回了用户数据源信息,接下来就是一些封装数据信息到 SecurityContextHolder 中等等一些操作。
  5. RememberMeServices 是核心类,Spring Security 中提供了两种实现类,一种是 TokenBasedRememberMeServices,这种方式就是将用户名、超时时间、密码等信息进行Base64编码即一些操作组成的令牌给服务器,验证令牌就是对浏览器中发来的进行反编码看看是否一致,这种方式安全度很低;另一种实现是 PersistentTokenBasedRememberMeServices,它内部依赖于 PersistentTokenRepository 仓库,提供了基于内存和基于 Jdbc 的实现,它比前一种安全,原因是它每次自动认证后会更新令牌(即Cookie),如果在自动认证过程中发现令牌不一致会及时剔除(即从PersistentTokenRepository仓库中删除),报Cookie被盗窃异常。
  6. PersistentTokenBasedRememberMeServices 中同 TokenBasedRememberMeServices 一样的是它也使用的是 Base64 编码后进行令牌设置,自动登录认证令牌的时候也需要解码;不同的是它是由两数据组成的(序列号(固定的),token(会变化的)),序列号在 onLoginSuccess 方法中生成的,然后存在浏览器上,在 processAutoLoginCookie 方法中不会改变这个序列号,只会变token,可以说浏览器Cookie解码后的序列号是固定死的(没有重新登录验证的情况下)。
  7. 使用起来很简单(一般使用的是PersistentTokenBasedRememberMeServices,且使用的仓库实现是 JdbcTokenRepositoryImpl),在配置 rememberMeConfigurer 时,配置一个 tokenRepository(PersistentTokenRepository) 即可,它会自动为我们配置 rememberMeServices 的,因为Spring Security就俩那实现,如果你配置了 PersistentTokenRepository,实质就默认你是使用了 PersistentTokenBasedRememberMeServices

OAuth2.0

OAuth全称【Open Authorization】是一个开放的授权标准,允许用户让第三方应用访问该用户在本应用中的数据,而无需将账号密码提供给第三方应用,OAuth通过颁发令牌的方式进行授权。所以OAuth是安全的。

每一个令牌授权一个特定网站在特定时间访问特定资源,OAuth 让用户可以授权第三方网站访问他们存储在另外服务提供者的某些特定信息,而非所有内容。为简化客户端开发提供了特定的授权流,包括 Web 应用、桌面应用、移动端应用等。

其中OAuth2.0是OAuth的延续版本,并不向前兼容,完全废弃了OAuth1.0

OAuth2.0中的角色

角色作用
Authorization Server认证服务器。用于认证用户,颁发token。如果客户端认证通过,则发放访问资源服务器的令牌
Resource Server资源服务器。拥有受保护资源,对非法请求拦截,对请求token解析。如果请求包含正确的访问令牌,则可以访问资源
Client客户端。它请求资源服务器时,会带上访问令牌,从而成功访问资源
Resource Owner资源拥有者。最终用户,他有访问资源的账号与密码(指的是用户)

实现流程

  • 申请对应认证服务器的OAuth权限
  • 引入 OAuth客户端依赖
  • 实现OAuth2.0授权
    • 获取第三方应用的授权码
    • 根据授权码,换取用户的信息

申请接入OAuth2.0

QQ:QQ互联,创建应用,需要有备案域名和网站或移动应用

微信:在微信开放平台上创建应用,需要有备案域名

还有钉钉、微博、github等各种各样的第三方平台提供了OAuth2.0授权认证,此处我们以gitee为例,因为它可以输入本地url路径,无需审核方便调用和测试。其他平台道理相同,无非换一套接口就可以了。

gitee上实现OAuth

首先看一下gitee的三方授权登录流程,此流程在giteeAPI手册中有体现,提供了授权码模式密码模式,此处我们使用的是授权码模式

  • 首先访问gitee的认证服务器,获取授权码
  • 再通过授权码访问应用服务器,获取用户的首先信息
  • 获取用户授权信息需要调用应用服务器,此时在我们的回调中实现
    在这里插入图片描述

此处会通过SpringSecurity默认的登录页面和自定义登录页面两种方式实现OAuth2.0的授权认证

首先创建应用
创建后获取应用信息
在这里插入图片描述

再点击【第三方应用】回到列表页,点击刚刚创建的应用,获取到里边的clientIdsecrect,写代码要用
在这里插入图片描述

基于默认页面

导入依赖

导入oauth2客户端依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>

yml配置

spring:
  security:
    oauth2:
      client:
        registration:
        # OAuth授权平台
          gitee:
            client-id: 你的clientId
            client-secret: 你的secret
            # 授权模式,固定为 authorization_code,其他值可以参考AuthorizationGrantType
            authorization-grant-type: authorization_code
            # 创建应用时设置的回调地址,接收授权码
            redirect-uri: http://localhost:8080/oauth/notify
            # 权限范围,可配置项在码云应用信息中查看
            scope:
              - user_info # 个人用户信息,和创建的应用中勾选的权限名一致
        provider:
          gitee:
            # 申请授权地址
            authorization-uri: https://gitee.com/oauth/authorize
            # 获取访问令牌地址
            token-uri: https://gitee.com/oauth/token
            # 查询用户信息地址
            user-info-uri: https://gitee.com/api/v5/user
            # 码云用户信息中的用户名字段
            user-name-attribute: name

获取用户信息

此处就是我们自己编写的回调接口,接收code

@GetMapping("oauth/notify")
public void oauthNotify(@RequestParam(name = "code") String code) {
    // 获取token
    Map<String ,Object> map = new HashMap<>();
    map.put("grant_type","authorization_code");
    map.put("code",code);
    map.put("client_id","14049509bafee62b5e8882e459d3ede580af7a952a525faed6757475bfa5b841");
    map.put("redirect_uri","http://localhost:8080/oauth/notify");
    map.put("client_secret","1011743c66551e7838a68e28bc9932965d539d110ab56c47eb3230d3dc5fba59");
    // 通过code获取access_token
    String post = HttpUtil.post("https://gitee.com/oauth/token", map);
    GiteeOAuthLoginVO loginVO = JSONUtil.toBean(post, GiteeOAuthLoginVO.class);
    // 获取用户信息
    String userInfoStr = HttpUtil.get("https://gitee.com/api/v5/user?access_token=" + loginVO.getAccessToken());
    // 将用户信息转换为实体
}

启用OAuth2.0

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    // 关闭 CSRF
    http.csrf((csrf)->csrf.disable());
    // authorizeHttpRequests:指定多个授权规则,按照顺序
    http.authorizeHttpRequests((req)-> req.requestMatchers("/oauth/notify").permitAll().anyRequest().authenticated());
    // 表单登录
    http.formLogin(Customizer.withDefaults());
    // 开启 OAuth2 登录
    http.oauth2Login(Customizer.withDefaults());
    return http.build();
}

访问8080接口
在这里插入图片描述

点击之后就会跳转到gitee的授权页面,点击登陆即可

如果未登录过gitee,则先登录gitee
在这里插入图片描述

如果登录了gitee则,直接可看到要获取的权限,点击同意授权即可
在这里插入图片描述

基于自定义页面

此时通过Thymeleaf实现,没有前后端分离,引入thymeleaf依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

删除掉在yml文件中对oauth的配置,我们要自己实现,在配置类中通过注册ClientRegistrationRepository对象实现

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    // 配置,启用rememberMe功能
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        // 关闭csrf
        http.csrf(csrf -> csrf.disable());
        // 配置请求拦截策略
        http.authorizeHttpRequests(auth -> auth.requestMatchers("/oauth/notify","/to_login").permitAll().anyRequest().authenticated());
        // 使用SpringSecurity默认的登录页面
        http.formLogin(Customizer.withDefaults());
        // 开启oauth登陆
        http.oauth2Login(Customizer.withDefaults());
        return http.build();
    }

    @Bean
    public ClientRegistrationRepository clientRegistrationRepository() {
        return new InMemoryClientRegistrationRepository(this.giteeClientRegistration());
    }
    // 配置giee的授权登录信息
    private ClientRegistration giteeClientRegistration() {
        return ClientRegistration.withRegistrationId("gitee")
                .clientId("clientId")
                .clientSecret("secret")
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
      			// 授权模式
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                .redirectUri("http://localhost:8080/login/oauth2/code/gitee")
            	// 授权范围,即创建应用时勾选的权限名
                .scope("user_info")
                .authorizationUri("https://gitee.com/oauth/authorize")
                .tokenUri("https://gitee.com/oauth/token")
            	// 获取用户信息的接口
                .userInfoUri("https://gitee.com/api/v5/user")
                .userNameAttributeName("name")
                .build();
    }
}

再编写Thymeleaf页面,这个页面要放在resources/templates目录下

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登陆页面</title>
</head>
<body>
    <!-- 这个链接从gitee的OAuth文档上获取的 需要修改为自己的clientId和回调地址-->
    <a href="https://gitee.com/oauth/authorize?
             client_id={clientId}&
             redirect_uri={你的回调地址}&
             response_type=code">gitee</a>
</body>
</html>

其他代码不需要修改,此时你就获取到用户的授权信息,可以将授权信息存到自己的数据库中,当做用户在你系统内的默认数据了。

退出登录

就是访问退出登录接口之后,需要做什么事情:

  • 清除cookie
  • 认证信息需要删除【redis】
  • 记录用户的退出日志
  • 跳转页面【前端做的,后端返回状态就可以了】

实现LogoutSuccessHandler做相关的逻辑操作

package com.stt.springsecuritydemo10.handler;

import cn.hutool.json.JSONUtil;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.util.HashMap;

@Component
@Slf4j
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {

    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        log.info("退出登录========》");
        HashMap<String, Object> map = new HashMap<>();
        map.put("code",200);
        map.put("msg","退出登录成功");
        response.setCharacterEncoding("UTF-8");
        response.getWriter().write(JSONUtil.toJsonStr(map));
    }
}

配置

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Autowired
    private MyLogoutSuccessHandler logoutSuccessHandler;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        // 配置退出登录
        http.logout(logout -> logout.logoutSuccessHandler(logoutSuccessHandler)
                    .deleteCookies("rememberMe"));
        return http.build();
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/622529.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

2005-2022年各省共同富裕指数数据(含原始数据+结果)

2005-2022年各省共同富裕指数数据&#xff08;含原始数据结果&#xff09; 1、时间&#xff1a;2005-2022年 2、来源&#xff1a;统计年鉴、各省年鉴 3、范围&#xff1a;31省 4、指标&#xff1a;年份、行政区划代码、地区、人均可支配收入_元、人均消费支出_元、恩格尔系…

ASP.NET Web Api 如何使用 Swagger 管理 API

前言 Swagger 是一个开源的框架&#xff0c;支持 OpenAPI 规范&#xff0c;可以根据 API 规范自动生成美观的、易于浏览的 API 文档页面&#xff0c;包括请求参数、响应示例等信息&#xff0c;并且&#xff0c;Swagger UI 提供了一个交互式的界面&#xff0c;可以帮助我们快速…

STK12 RPO模块学习(2)

一、Coast RPO Sequence 这个序列运行卫星直到它达到了下面三个条件之一。 1&#xff09;截至时间。2)圈数到达了限制。3&#xff09;其他条件&#xff0c;比如近地点。 默认情况下&#xff0c;Astrogator使用“Earth HPOP Default v10”预报器。你能够修改呈其他修改器。下…

C++之map和set 的封装

通过红黑树的学习&#xff08;C之红黑树-CSDN博客&#xff09;让我了解到map和set的底层如何实现&#xff0c;这一次我们来对map和set进行封装。 目录 1.map和set底层原理 2.map和set的定义 3.map和set的仿函数 4.map和set的插入 5.map和set的迭代器 5.1迭代器的构造 5.2…

解决Android手机无法通过蓝牙给win10 PC传送文件

&#xff08;一&#xff09;先配对设备&#xff0c;正常配对就可以 &#xff08;二&#xff09;打开系统设置&#xff0c;win搜索窗口搜索“设置” &#xff08;三&#xff09;搜索“蓝牙” &#xff08;四&#xff09;打开“蓝牙和其他设备”&#xff0c;点击“更多蓝牙设置”…

四款不同类型的企业防泄密软件推荐

在数字化快速发展的今天&#xff0c;企业数据的安全与保密显得愈发重要。防泄密软件作为一种专门的数据保护工具&#xff0c;已经逐渐成为企业不可或缺的安全屏障。本文将深入探讨防泄密软件对企业的意义&#xff0c;并介绍一些市面上主流的防泄密软件。 首先&#xff0c;防泄密…

【MySQL复合查询】

文章目录 一、基本的使用案例二、多表查询三、自连接四、子查询4.1单行子查询4.2多行子查询in关键字all关键字any关键字 4.3多列子查询4.4 在from子句中使用子查询 解决多表问题的本质五、合并查询1.union2.union all 一、基本的使用案例 注明&#xff1a;以下案例使用的均为一…

Spring-依赖查找

依赖查找 根据名称进行查找 实时查找 BeanFactory beanFactory new ClassPathXmlApplicationContext("beans.xml"); Object bean beanFactory.getBean("personHolder"); System.out.println(bean);xml如下: <bean id"person" class&qu…

【数据可视化01】matplotlib实例介绍2

目录 一、引言二、实例介绍1.箱线图2.热力图3.线条形式 一、引言 接着上一文章【数据可视化01】matplotlib实例介绍1继续介绍matplotlib的实例。 二、实例介绍 在matplotlib中&#xff0c;常用的图形类型包括&#xff1a; 箱线图&#xff08;Box plot&#xff09;&#xff1…

非预警,这3本TOP期刊,为何走到On Hold这步?

本周投稿推荐 SSCI • 2区社科类&#xff0c;3.0-4.0&#xff08;社科均可&#xff09; EI • 计算机工程类&#xff08;接收广&#xff0c;录用极快&#xff09; SCI&EI • 4区生物医学类&#xff0c;1.5-2.0&#xff08;录用率99%&#xff09; • 1区工程类&#…

如何用时尚新姿讲好中国品牌故事?

品牌建设在推动高质量发展中扮演了双重角色&#xff0c;既是高质量发展的重要“承载者”&#xff0c;也是强有力的“助推器”。5月10日-11日&#xff0c;中国时尚品牌URBAN REVIVO&#xff08;以下简称UR&#xff09;以中国品牌日为起点&#xff0c;联合天猫超级品牌日&#xf…

Spring框架深度解析:打造你的Java应用梦工厂

想要在Java企业级应用开发中大展身手&#xff1f;Spring框架的核心容器是你不可或缺的伙伴&#xff01; 文章目录 一. 引言1.1 介绍Spring框架的重要性1.2 阐述核心容器在Spring框架中的作用1.3 故事开端 二. 背景介绍2.1 描述Spring框架的发展历程2.2 概述Spring框架的主要特点…

数学老师们

小学三年级之前的数学老师&#xff0c;包括学前班给过我零分的数学老师&#xff0c;模样、姓名都不记得了。能回忆起来的最早的数学老师是四、五年级的李成娥老师。 李老师四十岁左右&#xff0c;短发&#xff0c;温和、爱笑&#xff0c;尤其是在班主任张老师的衬托下&#xf…

有必要买超声波洗眼镜机吗?力荐四款实力超群超声波清洗机

在日常生活中&#xff0c;眼镜不仅仅是我们视野的延展&#xff0c;像太阳眼镜&#xff0c;也是有着独特的作用。但是&#xff0c;在每天的使用过程中&#xff0c;眼镜片表面难免会有灰尘&#xff0c;污迹&#xff0c;甚至油渍&#xff0c;这些都会对镜片的材质产生一定的损伤&a…

宝塔面板各种疑难杂症处理命令教程

下载地址&#xff1a;宝塔面板各种疑难杂症处理命令教程 这份宝塔面板各种疑难杂症处理命令教程&#xff0c;可以解决市面上遇到的各种难题&#xff0c;建议有技术能行的下载使用&#xff0c;小白也可以下载来学习可以帮助你解决宝塔面板遇到的各种难题

网络端口占用问题的综合调研与解决方案

原创 Randy 拍码场 问题背景 去年底信息安全团队进行网络权限治理&#xff0c;要求所有应用实例使用静态IP&#xff0c;公网访问策略与静态IP绑定&#xff1b;之后实例重启时偶现“端口被占用”错误。通过分析总结应用日志&#xff0c;共有以下4种错误类型&#xff0c;实质都是…

Apple store 静安·苹果店欣赏

官网&#xff1a; https://www.apple.com/today/Apple 亚洲第一大商店&#xff1a;Apple 静安零售店现已在上海开幕 静安苹果欣赏

LVGL移植到ARM开发板(GEC6818)

源码下载&#xff1a;点击跳转 下载好三个文件后&#xff0c;将其解压缩&#xff0c;并合到一个文件夹里面—— 1、修改 Makefile 删除 -Wshift-negative-value 2、修改 main.c 3、修改 lv_drv_conf.h 在lv_drv_conf.h文件屏幕驱动文件刚好与开发板LCD驱动文件一致&#xff0c…

【操作指南】银河麒麟高级服务器操作系统内核升级——基于4.19.90-17升级

1. 升级清单 升级包及依赖包清单如下。 kernel ARM架构 kernel-core-4.19.90-23.18.v2101.ky10.aarch64.rpm kernel-modules-4.19.90-23.18.v2101.ky10.aarch64.rpm kernel-4.19.90-23.18.v2101.ky10.aarch64.rpm kernel-modules-extra-4.19.90-23.18.v2101.ky10.aarch64.r…

【小笔记】neo4j用load csv指令导入数据

【小笔记】neo4j用load csv指令导入数据 背景 很久没有用load CSV的方式导入过数据了因为它每次导入有数量限制&#xff08;印象中是1K还是1W&#xff09;&#xff0c;在企业中构建的图谱往往都是大规模的&#xff0c;此时通常采用的是Neo4j-admin import方式。最近遇到了一些…