SpringBoot Security实现单点登出并清除所有token

实现单点登出并清除所有token是一个比较常见的需求,Spring Security正是为此而生。下面是实现它的完整攻略:

步骤1:添加依赖

首先,在pom.xml中添加spring-boot-starter-security依赖:

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

该依赖包含了Spring Security和一些常用的依赖。

步骤2:配置security

接下来,我们需要在application.properties或application.yml中添加一些配置,如下所示:

### Session Management ###
server.servlet.session.timeout=1800
security.sessions=ALWAYS
security.max-sessions=1
security.session-registry-alias=sessionRegistry
security.logout-success-url=/
security.logout-url=/logout
logging.level.org.springframework.security=DEBUG

这些配置的含义如下:

  • server.servlet.session.timeout:定义会话的过期时间,以秒为单位。在本例中,会话过期时间为30分钟(1800秒)。
  • security.sessions=ALWAYS:定义始终创建会话。
  • security.max-sessions=1:允许同时存在的最大会话数量。在本例中,只允许一个会话存在。
  • security.session-registry-alias=sessionRegistry:定义Session Registry别名。
  • security.logout-success-url=/:定义退出后跳转到的URL地址。
  • security.logout-url=/logout:定义退出URL地址。
  • logging.level.org.springframework.security=DEBUG:定义调试日志级别,方便我们在开发时排查问题。

步骤3:定义全局过滤器

在WebSecurityConfig类中定义一个全局过滤器:

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled=true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private MyAuthenticationSuccessHandler authenticationSuccessHandler;

    @Autowired
    private MyAuthenticationFailureHandler authenticationFailureHandler;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/resources/**", "/signup", "/about").permitAll()
                .antMatchers("/admin/**").hasRole("ADMIN")
                .antMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')")
                .anyRequest().authenticated()
                .and()
            .formLogin()
                .loginPage("/login")
                .failureUrl("/login?error")
                .successHandler(authenticationSuccessHandler)
                .failureHandler(authenticationFailureHandler)
                .usernameParameter("username")
                .passwordParameter("password")
                .permitAll()
                .and()
            .logout()
                .invalidateHttpSession(true)
                .clearAuthentication(true)
                .deleteCookies("JSESSIONID")
                .logoutSuccessUrl("/login")
                .permitAll();
    }

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth
            .inMemoryAuthentication()
                .withUser("user").password("{noop}password").roles("USER")
                .and()
                .withUser("admin").password("{noop}password").roles("ADMIN");
    }

这里我们定义了若干条URL地址和其访问权限的规则,以及登录、失败和退出时的相关处理逻辑。

步骤4:定义SessionRegistry和SessionRegistryImpl

定义SessionRegistry:

@Component
public class SessionRegistryProvider implements ApplicationListener<AbstractAuthenticationEvent> {

    private SessionRegistry sessionRegistry = new SessionRegistryImpl();

    @Override
    public void onApplicationEvent(AbstractAuthenticationEvent event) {
        if (event instanceof AbstractAuthenticationFailureEvent) {
            return;
        }

        Authentication authentication = event.getAuthentication();
        UserDetails principal = (UserDetails) authentication.getPrincipal();
        String sessionId = SessionContextHolder.getSession().getId();

        List<SessionInformation> sessions = sessionRegistry.getAllSessions(principal, false);

        for (SessionInformation sessionInformation : sessions) {
            if (!sessionId.equals(sessionInformation.getSessionId())) {
                sessionInformation.expireNow();
            }
        }
    }

    public SessionRegistry getSessionRegistry() {
        return sessionRegistry;
    }

    public void setSessionRegistry(SessionRegistry sessionRegistry) {
        this.sessionRegistry = sessionRegistry;
    }
}

SessionRegistryImpl:

public class SessionRegistryImpl extends SessionRegistryImpl implements SessionRegistry {

    private Map<String, List<SessionInformation>> userSessions = new ConcurrentHashMap<>();

    /**
     * get all session information
     */
    @Override
    public List<SessionInformation> getAllSessions(Object principal, boolean includeExpiredSessions) {
        List<SessionInformation> list = new ArrayList<>();
        for (List<SessionInformation> sessionInformations : userSessions.values()) {
            list.addAll(sessionInformations);
        }
        return list;
    }

    /**
     * get list of sessions that should be marked as expired
     */
    @Override
    public List<Object> getAllPrincipals() {
        return new ArrayList<>(userSessions.keySet());
    }

    /**
     * get session information for specific user
     */
    @Override
    public List<SessionInformation> getSessions(Object principal, boolean includeExpiredSessions) {
        List<SessionInformation> sessionInformations = userSessions.get(getKey(principal));
        if (sessionInformations == null) {
            return new ArrayList<>();
        }

        List<SessionInformation> list = new ArrayList<>();

        for (SessionInformation sess : sessionInformations) {
            if (includeExpiredSessions || !sess.isExpired()) {
                list.add(sess);
            }
        }
        return list;
    }

    /**
     * remove session information from buffer
     */
    @Override
    public void removeSessionInformation(String sessionId) {
        SessionInformation info = getSessionInformation(sessionId);
        if (info == null) {
            return;
        }

        List<SessionInformation> usersSessions = userSessions.get(getKey(info.getPrincipal()));
        usersSessions.remove(info);

        if (usersSessions.size() == 0) {
            userSessions.remove(getKey(info.getPrincipal()));
        }
    }

    /**
     * updates an existing session with a new lastRequest time
     */
    @Override
    public void refreshLastRequest(String sessionId) {
        SessionInformation info = getSessionInformation(sessionId);
        if (info == null) {
            return;
        }
        info.refreshLastRequest();
    }

    /**
     * register new session information
     */
    @Override
    public void registerNewSession(String sessionId, Object principal) {
        Assert.hasText(sessionId, "SessionId required as per interface contract");
        Assert.notNull(principal, "Principal required as per interface contract");

        if (logger.isDebugEnabled()) {
            logger.debug("Registering session " + sessionId + ", for principal " + principal);
        }

        List<SessionInformation> sessionInformations = userSessions.get(getKey(principal));
        if (sessionInformations == null) {
            sessionInformations = new CopyOnWriteArrayList<>();
            userSessions.put(getKey(principal), sessionInformations);
        }

        sessionInformations.add(new SessionInformation(principal, sessionId, new Date()));
    }

    /**
     * get session information for specific session
     */
    public SessionInformation getSessionInformation(String sessionId) {
        for (List<SessionInformation> sessionInformations : userSessions.values()) {
            for (SessionInformation sess : sessionInformations) {
                if (sess.getSessionId().equals(sessionId)) {
                    return sess;
                }
            }
        }

        return null;
    }

    /**
     * generate key for user sessions map
     */
    private String getKey(Object principal) {
        return principal.toString();
    }
}

步骤5:定义SessionContextHolder和JwtUtil

定义SessionContextHolder:

@Component
public class SessionContextHolder implements HttpSessionListener {

    private static final AtomicLong sessionIds = new AtomicLong(0);

    private static final ThreadLocal<HttpSession> sessionHolder = new ThreadLocal<>();
    private static final ThreadLocal<Long> sessionIdsHolder = ThreadLocal.withInitial(sessionIds::incrementAndGet);

    @Override
    public void sessionCreated(HttpSessionEvent event) {
        sessionHolder.set(event.getSession());
    }

    @Override
    public void sessionDestroyed(HttpSessionEvent event) {
        sessionHolder.remove();
    }

    public static HttpSession getSession() {
        return sessionHolder.get();
    }

    public static String getSessionId() {
        return getSession().getId();
    }

    public static Long getNewSessionId() {
        Long sessionId = sessionIdsHolder.get();
        sessionIdsHolder.remove();
        return sessionId;
    }
}

定义JwtUtil:

@Component
public class JwtUtil {

    private static final String SECRET_KEY = "123456";

    public static String generateToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>();
        return createToken(claims, userDetails.getUsername());
    }

    private static String createToken(Map<String, Object> claims, String subject) {

        return Jwts.builder()
                .setClaims(claims)
                .setSubject(subject)
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10))
                .signWith(SignatureAlgorithm.HS512, SECRET_KEY.getBytes())
                .compact();
    }

    public static Boolean validateToken(String token, UserDetails userDetails) {
        final String username = getUsernameFromToken(token);
        return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
    }

    private static Boolean isTokenExpired(String token) {
        final Date expiration = getExpirationDateFromToken(token);
        return expiration.before(new Date());
    }

    private static Date getExpirationDateFromToken(String token) {
        return Jwts.parser().setSigningKey(SECRET_KEY.getBytes()).parseClaimsJws(token).getBody().getExpiration();
    }

    private static String getUsernameFromToken(String token) {
        return Jwts.parser().setSigningKey(SECRET_KEY.getBytes()).parseClaimsJws(token).getBody().getSubject();
    }
}

步骤6:完成逻辑

最后,我们只需要在退出登录时,清除所有Token,并使已有的会话过期即可。代码如下所示:

@Component
public class CustomLogoutSuccessHandler implements LogoutSuccessHandler {

    @Autowired
    private SessionRegistry sessionRegistry;

    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response,
                                Authentication authentication) throws IOException, ServletException {
        Cookie[] cookies = request.getCookies();
        if (cookies != null) {
            for (Cookie cookie : cookies) {
                cookie.setMaxAge(0);
                cookie.setValue(null);
                cookie.setPath("/");
                response.addCookie(cookie);
            }
        }

        String sessionId = request.getSession().getId();
        SessionInformation sessionInformation = sessionRegistry.getSessionInformation(sessionId);
        if (sessionInformation != null) {
            sessionInformation.expireNow();
            sessionRegistry.removeSessionInformation(sessionId);
        }

        response.sendRedirect("/login");
    }
}

至此,我们已经完成了SpringBoot Security实现单点登出并清除所有Token的全部过程。下面是示例说明。

示例1:

@Component
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    @Autowired
    private JwtUtil jwtUtil;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                        Authentication authentication) throws IOException, ServletException {
        String token = jwtUtil.generateToken((UserDetails) authentication.getPrincipal());
        response.setContentType("application/json;charset=utf-8");
        PrintWriter out = response.getWriter();
        out.write("{\"code\":200,\"msg\":\"ok\",\"token\":\"" + token + "\"}");
        response.setHeader("token", token);
    }

}

在登录成功时,我们生成一个Token,并将Token添加到请求头中,以便后续访问时进行认证。

示例2:

@Controller
public class LogoutController {

    @GetMapping("/logout")
    public String logout(HttpServletRequest request, HttpServletResponse response) throws Exception {
        return "/login";
    }
}

此处,我们定义一个/logout的URL地址,当用户访问该地址时,系统将清除所有Token,并使已有的会话过期。如果你希望在清除Token时可以自定义逻辑,则可以在CustomLogoutSuccessHandler类中添加相应的处理代码,以满足你的要求。

本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:SpringBoot Security实现单点登出并清除所有token - Python技术站

(0)
上一篇 2023年6月3日
下一篇 2023年6月3日

相关文章

  • Java 超详细讲解异常的处理

    Java 超详细讲解异常的处理 什么是异常? 在 Java 中,异常指的是程序在运行过程中发生了意外情况或错误,导致程序无法继续运行的情况。比如数组访问越界、空指针等。 异常的分类 在 Java 中,异常分为两类:受检异常和非受检异常。 受检异常(Checked Exception) 受检异常指的是在编译时就能够发现的异常,需要在代码中显式的进行处理。比如读…

    Java 2023年5月19日
    00
  • java查找字符串中的包含子字符串的个数实现代码

    下面是“Java查找字符串中的包含子字符串的个数实现代码”的完整攻略。 问题描述 我们需要写一个Java程序,用于在一个字符串中查找指定的子字符串,并返回该子字符串在源字符串中出现的次数。 解决方案 我们可以使用Java内置的字符串函数或正则表达式来实现这个功能,下面是两种不同的方法: 方法一:使用String函数 我们可以使用String类中提供的inde…

    Java 2023年5月27日
    00
  • 解决spring boot 1.5.4 配置多数据源的问题

    下面是解决Spring Boot 1.5.4配置多数据源的步骤: 1. 添加多数据源配置 打开Spring Boot项目的配置文件application.properties或application.yml,在其中添加多数据源的配置。示例代码如下(假设需要配置两个数据源:db1和db2): spring: datasource: db1: url: jdbc…

    Java 2023年6月16日
    00
  • 深入解析Spring Boot 的SPI机制详情

    深入解析Spring Boot 的SPI机制详情 在Spring Boot中,SPI是一种Java的扩展机制,它让应用程序可以在运行时动态加载一个类或多个类实现的接口,并执行相应的操作。下面我们将深入探究Spring Boot的SPI机制的实现细节。 什么是SPI机制 SPI,全称为Service Provider Interface,是一种Java的扩展机…

    Java 2023年5月20日
    00
  • J2SE中的序列化之继承

    J2SE中的序列化是将对象转换成字节流,用于对象的存储和传输。而在序列化对象时,如果该对象实现了Serializable接口,那么子类也会自动实现序列化,这就是所谓的“继承序列化”。 下面通过示例说明继承序列化的几个要点: 1.子类序列化时父类属性的序列化与反序列化: public class Parent implements Serializable{ …

    Java 2023年6月15日
    00
  • Spring Boot Cache使用方法整合代码实例

    下面我将详细讲解“Spring Boot Cache使用方法整合代码实例”的完整攻略。 一、什么是Spring Boot Cache Spring Boot Cache是Spring Boot中的缓存框架,它提供了一种简单的方式来缓存数据的读取结果,从而减少不必要的计算并提升应用程序的性能。 二、Spring Boot Cache使用方法 1. 引入依赖 在…

    Java 2023年5月31日
    00
  • SpringBoot整合Web开发之Json数据返回的实现

    下面我来详细讲解一下“SpringBoot整合Web开发之Json数据返回的实现”的完整攻略。 1. 概述 在Web开发中,我们通常需要将Java对象转换成Json数据格式再返回给前端,SpringBoot提供了很方便的解决方案。以下将分别介绍使用SpringBoot实现json数据返回的两种方法:@ResponseBody注解和ResponseEntity…

    Java 2023年5月19日
    00
  • Spring Boot如何优化内嵌的Tomcat示例详解

    针对这个问题,我来详细讲解一下Spring Boot如何优化内嵌的Tomcat,包含以下内容: 1. 优化内嵌Tomcat的原因 Spring Boot在内嵌Tomcat作为HTTP服务器的情况下,处理请求效率较低,主要原因是默认的Tomcat设置了大量的属性,例如发送缓存和接收缓存大小、最大线程数等,这些设置并不一定适用于所有应用程序。因此,我们需要对内嵌…

    Java 2023年5月19日
    00
合作推广
合作推广
分享本页
返回顶部