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日

相关文章

  • 解决mybatis-plus自动配置的mapper.xml与java接口映射问题

    针对“解决mybatis-plus自动配置的mapper.xml与java接口映射问题”,我给出以下完整攻略,主要分为两个部分: 1. 配置XML文件路径 mybatis-plus默认情况下会在classpath:/mapper/下寻找对应的mapper.xml文件,因此需要确保xml文件路径正确。 可以使用如下方式在application.yml或appl…

    Java 2023年5月20日
    00
  • Java中常见的对象转换工具

    Java中常见的对象转换工具有很多种,其中比较常用的包括如下几种: Gson:Google开发的一款Java JSON处理工具,可以将JSON字符串与Java对象互相转换。 转换示例: import com.google.gson.Gson; public class Example { public static void main(String[] ar…

    Java 2023年5月19日
    00
  • Spring Boot+Jpa多数据源配置的完整步骤

    下面是Spring Boot+Jpa多数据源配置的完整攻略: 配置文件 首先需要在application.properties 或者 application.yml 配置文件中进行多数据源的配置。示例如下: # 数据源 1 spring.datasource.first.url=jdbc:mysql://localhost:3306/first_databa…

    Java 2023年5月20日
    00
  • 常见的Java ORM框架有哪些?

    Java ORM(Object-Relational Mapping)框架是用于简化Java应用程序与关系数据库之间的数据映射、数据管理和数据操作的工具,常见的Java ORM框架有以下几种: Hibernate:Hibernate是一个广泛应用的Java ORM框架,支持JPA(Java Persistence API)规范,其主要优点是开发效率高、功能强…

    Java 2023年5月11日
    00
  • Springboot拦截器如何获取@RequestBody参数

    下面是关于Spring Boot拦截器如何获取@RequestBody参数的攻略。 什么是拦截器 拦截器是Spring框架中的一个组件,它是在请求到达Controller之前或离开Controller之后执行的代码块。拦截器主要用于对请求进行预处理和后处理,在预处理中可以实现一些安全性检查和参数校验等操作,而后处理中可以对响应结果进行处理。 如何获取@Req…

    Java 2023年5月20日
    00
  • Java中字符数组、String类、StringBuffer三者之间相互转换

    Java中字符数组、String类、StringBuffer三者之间可以互相转换,下面分别介绍其转换方法。 1、字符数组与String类之间的转换 1.1、字符数组转String char[] charArray = {‘h’, ‘e’, ‘l’, ‘l’, ‘o’}; String str = new String(charArray); 1.2、Stri…

    Java 2023年5月27日
    00
  • Java Http接口加签、验签操作方法

    关于Java Http接口加签、验签操作方法的完整攻略,可以分为以下几个部分: 什么是接口加签、验签? 在网络通信中,为了防止数据伪造、篡改等安全问题,需要使用加密、签名等方式来保护数据安全。接口加签、验签是其中的一种方式。简单来说,就是在数据通信的过程中,在数据中加入签名信息,用于识别数据的真实性。接口加签指的是计算签名,并将签名在请求头或请求参数中传输。…

    Java 2023年5月26日
    00
  • 解读动态数据源dynamic-datasource-spring-boot-starter使用问题

    我来为您详细讲解“解读动态数据源dynamic-datasource-spring-boot-starter使用问题”的完整攻略。 一、什么是dynamic-datasource-spring-boot-starter dynamic-datasource-spring-boot-starter是一款基于SpringBoot的动态多数据源框架,能够帮助您快速…

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