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日

相关文章

  • jsp struts1 标签实例详解第2/2页

    下面我将详细讲解JSP Struts1标签实例详解的完整攻略。该攻略分为两页,这里我将着重对第二页进行讲解。 一、JSP Struts1标签实例详解(第2/2页) 本文主要对Struts标签库进行介绍,讲解它们的使用方法和常用属性。 1. html:submit(表单提交按钮) html:submit标签用于创建表单提交按钮。以下是html:submit标签…

    Java 2023年6月15日
    00
  • Java之进程和线程的区别

    Java之进程和线程的区别 在Java中,进程和线程是很重要的概念。现在我们将详细讲解它们的区别。 什么是进程? 进程是指在内存中运行的程序的实例。每个进程都有自己的内存空间和系统资源,包括CPU时间、文件句柄等。每个进程都是独立的,它们不能直接互相访问对方的内存空间和系统资源。 Java中可以通过Process类实现对进程的操作。例如,可以使用Proces…

    Java 2023年5月18日
    00
  • Java:如何加密或解密PDF文档?

    在工作中,我们会将重要的文档进行加密,并且设置用户的访问权限,其他外部人员均无法打开,只有获取该权限的用户才有资格打开文档。此外,限制用户的使用权限,极大程度上阻止了那些有意要篡改、拷贝其中内容的人,提高文档的安全性。与此同时,文档加密的另一大作用是为了防止丢失,因为可能存在员工出差或离职时,将文档有意或无意的删除,造成文档丢失的现象,从而导致公司的业务和形…

    Java 2023年4月18日
    00
  • jdbc中自带MySQL 连接池实践示例

    下面是 “jdbc中自带MySQL 连接池实践示例” 的详细攻略: 准备工作 下载并安装 MySQL 数据库,创建一个名为 test 的数据库并创建一个名为 user 的表,包含 id、name、age 三个字段。 下载并安装 JDK,确认环境变量配置正确。 示例一:使用 DriverManager 方式连接数据库 导入 JDBC 驱动 // 导入MySQL…

    Java 2023年6月16日
    00
  • MyBatis的逆向工程详解

    MyBatis的逆向工程详解 什么是MyBatis逆向工程? MyBatis逆向工程是指根据数据库中的表结构生成MyBatis对应的Mapper接口以及对应的Mapper XML文件。如果手写这些代码,需要考虑很多细节,编写起来比较繁琐和容易出错,而逆向工程则可以自动化地生成这些代码。逆向工程可以大大提高开发效率,并且保证生成的代码的准确性。 MyBatis…

    Java 2023年5月19日
    00
  • 从零开始在Centos7上部署SpringBoot项目

    从零开始在CentOS7上部署Spring Boot项目,大致分为以下几个步骤: 安装Java环境 在CentOS7上部署Spring Boot项目,首先需要安装Java环境。可以通过以下命令安装: yum install java-1.8.0-openjdk-devel 安装完成后,可以通过以下命令查看Java版本: java -version 安装Mav…

    Java 2023年5月20日
    00
  • java日期时间格式化@JsonFormat与@DateTimeFormat的使用

    下面就为您详细讲解“java日期时间格式化@JsonFormat与@DateTimeFormat的使用”的完整攻略。 一、前言 在开发 Java 项目时,常常需要对日期时间进行格式化。这时,我们就可以使用@JsonFormat和@DateTimeFormat这两个注解来实现。 二、@JsonFormat注解 @JsonFormat注解是用来指定Java对象的…

    Java 2023年5月20日
    00
  • Java正则表达式入门基础篇(新手必看)

    让我来为你详细讲解一下“Java正则表达式入门基础篇(新手必看)”这篇文章的完整攻略。 标题 首先,我们来看一下文章的标题:“Java正则表达式入门基础篇(新手必看)”。这个标题十分的清晰明了,表明了本文的主题和受众人群。接下来我们来一步一步的解析这篇文章的内容: 介绍 首先,文章介绍了正则表达式的定义,即一种用来匹配字符串的文本模式。同时也解释了正则表达式…

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