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

news2024/12/26 9:20:54

本文章对应视频可在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();
    }
}

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

AI+文旅|当智慧遇见风景,感受文旅新体验

今年的五一假期,公众出游热度持续升温&#xff0c;全国多地景区再现“人山人海”&#xff0c;在这样的背景下&#xff0c;促使文旅行业不断通过数字化手段&#xff0c;提升旅游体验质量、探索新的服务方式&#xff0c;AI技术的加入为旅游业带来了革命性的变化。智能导游、智能推…

FOTS:一种用于机器人操作技能Sim2Real学习的快速光学触觉仿真器

类 GelSight的视触觉传感器具有高分辨率和低制造成本的优势&#xff0c;但是在与现实中的物体进行频繁接触时易受磨损。而触觉仿真器可大幅降低硬件成本&#xff0c;同时为后续技能学习任务提供仿真训练环境。为此&#xff0c;来自东南大学自动化学院的钱堃副教授研究团队和伦敦…

大厂常见算法50题-两数相加

专栏持续更新50道算法题&#xff0c;都是大厂高频算法题&#xff0c;建议关注, 一起巧‘背’算法! 文章目录 题目解法总结 题目 解法 定义一个节点pre&#xff0c;用于初始化结果链表的头部&#xff0c;cur指向pre&#xff0c;它将在遍历过程中用于构建新的链表。初始化进位变…

基于单片机的直流电机测速装置研究与设计

摘要: 基于单片机的直流电机测速装置采用了对直流电机的中枢供电回路串联取样电阻的方式实现对电机转速的精确实时测量。系统由滤波电路、信号放大电路、单片机控制电路以及稳压电源等功能模块电路构成。工作过程中高频磁环作为载体&#xff0c;利用电磁感应的基本原理对直流电…

使用Flask部署Web应用:从入门到精通

文章目录 第一部分&#xff1a;准备工作第二部分&#xff1a;部署Flask应用到AWS部署到AWS Lambda 第三部分&#xff1a;部署Flask应用到腾讯云服务器部署到腾讯云服务器 第四部分&#xff1a;优化和扩展结论 在现代软件开发中&#xff0c;Web应用的部署是一个至关重要的环节。…

面试题:调整数字顺序,使奇数位于偶数前面

题目&#xff1a; 输入一个整数数组&#xff0c;实现一个函数&#xff0c;来调整该数组中数字的顺序 使得所有奇数位于数组的前半部分&#xff0c;所有偶数位于数组的后半部分 算法1&#xff1a; 利用快速排序的一次划分思想&#xff0c;从2端往中间遍历 时间复杂度&#x…

【漏洞复现】泛微OA E-Cology ResourceServlet文件读取漏洞

漏洞描述&#xff1a; 泛微OA E-Cology是一款面向中大型组织的数字化办公产品&#xff0c;它基于全新的设计理念和管理思想&#xff0c;旨在为中大型组织创建一个全新的高效协同办公环境。泛微OA E-Cology ResourceServlet存在任意文件读取漏洞&#xff0c;允许未经授权的用户…

网络安全之OSPF进阶

该文针对OSPF进行一个全面的认识。建议了解OSPF的基础后进行本文的一个阅读能较好理解本文。 OSPF基础的内容请查看&#xff1a;网络安全之动态路由OSPF基础-CSDN博客 OSPF中更新方式中的触发更新30分钟的链路状态刷新。是因为其算法决定的&#xff0c;距离矢量型协议是边算边…

【java-数据结构14-双向链表的增删查改2】

上一篇文章中&#xff0c;我们已经对双向链表进行一些基本操作&#xff0c;本篇文章我们继续通过对链表的增删查改来加深对链表的理解~同时有任何不懂的地方可以在评论区留言讨论&#xff0c;也可以私信小编~觉得小编写的还可以的可以留个关注支持一下~话不多说正片开始~ 注意…

「JavaEE」多线程案例分析3:线程池

&#x1f387;个人主页&#xff1a;Ice_Sugar_7 &#x1f387;所属专栏&#xff1a;JavaEE &#x1f387;欢迎点赞收藏加关注哦&#xff01; 线程池 &#x1f349;简介&#x1f349;标准库中的线程池&#x1f349;Executors&#x1f349;实现一个简单的线程池 &#x1f349;简介…

初始Django

初始Django 一、Django的历史 ​ Django 是从真实世界的应用中成长起来的&#xff0c;它是由堪萨斯&#xff08;Kansas&#xff09;州 Lawrence 城中的一个网络开发小组编写的。它诞生于 2003 年秋天&#xff0c;那时 Lawrence Journal-World 报纸的程序员 Adrian Holovaty 和…

识别AI论文生成内容,降低论文高AI率

AI写作工具能帮我们在短时间内高效生成一篇毕业论文、开通报告、文献综述、任务书、调研报告、期刊论文、课程论文等等&#xff0c;导致许多人开始使用AI写作工具作为撰写学术论文的辅助手段。而学术界为了杜绝此行为&#xff0c;开始使用AIGC检测系统来判断文章是由AI生成还是…

机器学习中的聚类

目录 认识聚类算法 聚类算法API的使用 聚类算法实现流程 聚类算法模型评估 认识聚类算法 聚类算法是一种无监督的机器学习算法。 它将一组数据分成若干个不同的群组&#xff0c;使得每个群组内部的数据点相似度高&#xff0c;而不同群组之间的数据点相似度低。常用的相似…

【3dmax笔记】028:倒角的使用方法

一、倒角描述 在3dmax中创建倒角效果可以通过多种方法实现,以下是几种常见的方法: 使用倒角修改器。首先创建一个图形(如矩形和圆),然后对齐它们,将它们转化为可编辑样条线,并附加在一起,选择要倒角的边缘,然后使用倒角修改器来调整高度、轮廓等参数。使用倒角剖面修…

Davinci工程vBaseEnv模块讲解

配合的模块 要正常使用vBaseEnv模块&#xff0c;要同时使能EcuC、OS、vBRS和vLinkGen模块。 OS是操作系统&#xff0c;除了FBL以外&#xff0c;其他都需要用到OS。 vBaseEnv是基础环境 vBRS是基本运行系统 vLinkGen脚本链接生成器 EcuC是ECU配置 EcuC配置 需要配合vBaseEnv模…

程序员的神奇应用:从代码创造到问题解决的魔法世界之持续集成/持续部署

文章目录 持续集成/持续部署 在软件开发的海洋中&#xff0c;程序员的实用神器如同航海中的指南针&#xff0c;帮助他们导航、加速开发、优化代码质量&#xff0c;并最终抵达成功的彼岸。这些工具覆盖了从代码编写、版本控制到测试和部署的各个环节。 在当今数字化的世界里&…

数字水印 | Arnold 变换的 Python 代码实现

&#x1f96d; 参考博客&#xff1a; Arnold 阿诺德置乱&#xff08;猫脸变换&#xff09;图像盲水印注入预处理&#xff08;Python&#xff09; 1 回顾&#xff1a;Arnold 公式 A r n o l d \mathsf{Arnold} Arnold 变换公式如下&#xff1a; [ x n 1 y n 1 ] [ 1 b a a b…

搜索引擎的设计与实现(二)

目录 3 搜索引擎的基本原理 3.1搜索引擎的基本组成及其功能 l.搜索器 (Crawler) 2.索引器(Indexer) 3.检索器(Searcher) 4.用户接口(UserInterface) 3.2搜索引擎的详细工作流程 4 系统分析与设计 4.1系统分析 4.2系统概要设计 4.2系统实现目标 前面内容请移步 搜索引…

力扣HOT100 - 70. 爬楼梯

解题思路&#xff1a; 动态规划 注意 if 判断和 for 循环 class Solution {public int climbStairs(int n) {if (n < 2) return n;int[] dp new int[n 1];dp[1] 1;dp[2] 2;for (int i 3; i < n; i) {dp[i] dp[i - 1] dp[i - 2];}return dp[n];} }

Co-Driver:基于 VLM 的自动驾驶助手,具有类人行为并能理解复杂的道路场景

24年5月来自俄罗斯莫斯科研究机构的论文“Co-driver: VLM-based Autonomous Driving Assistant with Human-like Behavior and Understanding for Complex Road Scenes”。 关于基于大语言模型的自动驾驶解决方案的最新研究&#xff0c;显示了规划和控制领域的前景。 然而&…