基于springboot和redis实现单点登录

接下来我将详细讲解“基于springboot和redis实现单点登录”的完整攻略,过程中将包含两个示例说明。

1. 基础知识

1.1 什么是单点登录?

单点登录(Single Sign-On,简称SSO)是一种允许用户使用一个帐户和密码访问多个应用程序的技术。在实现SSO时,用户只需要在一个应用程序中进行身份验证,然后就可以访问其他应用程序,而无需再次输入身份验证凭据。

1.2 为什么要使用redis?

在实现SSO时,我们需要共享用户的登录状态信息,以便在多个应用程序中使用。为了实现这一点,我们可以使用Cookie或Session,但是当应用程序数量增加时,这种方法可能在性能和安全方面受到挑战。因此,我们可以使用分布式缓存来存储状态信息,并使其在多个应用程序之间共享。Redis是一个被广泛使用的开源、基于内存的分布式缓存解决方案,提供快速读写能力和良好的可扩展性。

1.3 使用技术

在此攻略中,我们将使用以下技术:

  • SpringBoot:一种基于Spring框架的快速开发应用程序的技术。
  • Redis:一个基于内存的开源分布式键值对存储系统。
  • Spring Security:一个在Spring应用程序中提供安全性身份验证和授权的框架。
  • JWT:一种用于客户端-服务器通信的开放标准,用于在Web应用程序中传输信息作为JSON对象。

2. 示例1:单个应用程序上的实现

在这个示例中,我们将演示如何在单个应用程序中实现基于Spring Boot和Redis的单点登录。

2.1 创建SpringBoot项目

首先,我们需要使用SpringBoot创建一个新的项目,具体操作可以使用IDE工具(如IntelliJ IDEA)或者使用Spring官网进行创建。在创建过程中需要引入Spring Security和Redis的依赖。

2.2 配置Redis连接

接下来,我们需要创建一个Redis连接,并将其配置为Spring应用程序的默认缓存管理器。在Spring Boot中,我们可以通过配置文件轻松完成此操作。在application.properties文件中添加以下配置:

# Redis连接配置
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.database=0

2.3 创建登录页面

接下来,我们需要创建一个登录页面,让用户在此处输入用户名和密码。可以使用标准HTML表单和Spring Security进行身份验证。在此示例中,我们假设用户在登录成功后将被重定向到主页。

<!DOCTYPE html>
<html>
<head>
    <title>登录</title>
</head>
<body>
    <h1>登录</h1>
    <form method="post" action="/login">
        <!--请输入用户名:<input type="text" name="username" required><br>-->
        <!--请输入密码:<input type="password" name="password" required><br>-->
        请输入用户名:<input type="text" name="username" required>
        <br>
        请输入密码:<input type="password" name="password" required>
        <br>
        <button type="submit">登录</button>
    </form>
</body>
</html>

2.4 实现身份验证和创建Token

然后,我们需要实现身份验证并创建Token,使用Spring Security进行身份验证。在这个例子中,用户名和密码将被存储在内存数据库(后面的章节中会展示如何使用数据库进行身份验证),并且在验证成功后,用户将被重定向到主页,并得到一个JWT Token。

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/login").permitAll()
                .anyRequest().authenticated()
                .and()
            .formLogin()
                .loginPage("/login")
                .defaultSuccessUrl("/")
                .permitAll()
                .and()
            .logout()
                .permitAll();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // username和password存储在内存数据库中
        auth
            .inMemoryAuthentication()
                .withUser("user")
                .password("password")
                .roles("USER");
    }

    @Bean
    public TokenProvider tokenProvider() {
        return new TokenProvider();
    }
}

public class TokenProvider {
    private static final Logger logger = LoggerFactory.getLogger(TokenProvider.class);

    private final String secretKey;
    private final long tokenValidityInSeconds;

    public TokenProvider() {
        this.secretKey = "mySecretKey";
        this.tokenValidityInSeconds = 3600;
    }

    public String createToken(String username) {
        Date now = new Date();
        Date validity = new Date(now.getTime() + this.tokenValidityInSeconds * 1000);

        return Jwts.builder()
                .setSubject(username)
                .setIssuedAt(now)
                .signWith(SignatureAlgorithm.HS256, secretKey)
                .setExpiration(validity)
                .compact();
    }

    public Authentication getAuthentication(String token) {
        Claims claims = Jwts.parser()
                .setSigningKey(secretKey)
                .parseClaimsJws(token)
                .getBody();

        List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER");

        User principal = new User(claims.getSubject(), "", authorities);

        return new UsernamePasswordAuthenticationToken(principal, "", authorities);
    }

    public boolean validateToken(String token) {
        try {
            Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);
            return true;
        } catch (SignatureException ex) {
            logger.error("Invalid JWT signature");
        } catch (MalformedJwtException ex) {
            logger.error("Invalid JWT token");
        } catch (ExpiredJwtException ex) {
            logger.error("Expired JWT token");
        } catch (UnsupportedJwtException ex) {
            logger.error("Unsupported JWT token");
        } catch (IllegalArgumentException ex) {
            logger.error("JWT claims string is empty.");
        }
        return false;
    }
}

2.5 存储Token

现在,我们需要将Token存储到Redis缓存中,以便将Token共享到其他应用程序中。在这个例子中,我们在用户登录成功后创建Token,并将其存储到Spring默认的CacheManager中。CacheManager本质上就是一个Spring容器,用于管理Spring Cache抽象的缓存和缓存管理器的工厂。

@Controller
public class LoginController {

    private TokenProvider tokenProvider;
    private CacheManager cacheManager;

    public LoginController(TokenProvider tokenProvider, CacheManager cacheManager) {
        this.tokenProvider = tokenProvider;
        this.cacheManager = cacheManager;
    }

    @PostMapping("/login")
    public String login(HttpServletRequest request, HttpServletResponse response) {
        String username = request.getParameter("username");
        String password = request.getParameter("password");
        Authentication authentication = new UsernamePasswordAuthenticationToken(username, password);

        try {
            authentication = new TokenProvider().authenticate(authentication);
            SecurityContextHolder.getContext().setAuthentication(authentication);

            String token = tokenProvider.createToken(username);
            cacheManager.getCache("tokens").put(token, username);

            CookieUtils.addCookie(response, "token", token, 1800);
            return "redirect:/";
        } catch (BadCredentialsException e) {
            return "redirect:/login?error";
        }
    }
}

2.6 配置CacheManager

现在,我们需要将Spring默认的CacheManager替换为使用Redis作为缓存的管理器。我们可以通过向Spring应用程序添加RedisCacheManager bean来实现这一点。在这个例子中,我们将Redis缓存的过期时间设置为30分钟,以确保Token不会一直保留在Redis缓存中。

@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
    RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofMinutes(30L))
            .disableCachingNullValues();

    return RedisCacheManager.builder(redisConnectionFactory)
            .cacheDefaults(redisCacheConfiguration)
            .transactionAware()
            .build();
}

示例2:多个应用程序上的实现

在这个示例中,我们将扩展第一个示例并演示如何在多个应用程序中实现基于Spring Boot和Redis的单点登录。

2.7 创建多个应用程序

首先,我们需要创建多个SpringBoot应用程序来充当我们的多个应用程序。在这个示例中,我们将创建两个应用程序:sso-client1和sso-client2。

2.8 修改配置文件

接下来,我们需要修改这些应用程序的application.properties文件,以使用Redis作为缓存管理器。我们还需要配置这些应用程序允许哪个地址作为已经登录用户的重定向地址:

# Redis连接配置
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.database=0

# SSO配置
sso.redirect.url=http://localhost:{server.port}
sso.include.urls=/, /homepage

2.9 创建拦截器

然后,我们需要创建一个拦截器来截获要访问的URL,并检查用户是否已登录。如果用户已登录,则拦截器将允许请求继续,并将用户信息注入请求中。否则,拦截器将将请求重定向到登录页面。

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    private TokenProvider tokenProvider;
    private RestTemplate restTemplate;
    private String redirectUrl;
    private String[] includeUrls;

    public SecurityConfig(TokenProvider tokenProvider, RestTemplate restTemplate,
            @Value("${sso.redirect.url}") String redirectUrl, @Value("${sso.include.urls}") String[] includeUrls) {
        this.tokenProvider = tokenProvider;
        this.restTemplate = restTemplate;
        this.redirectUrl = redirectUrl;
        this.includeUrls = includeUrls;
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring()
                .antMatchers("/health")
                .antMatchers("/info")
                .antMatchers("/css/**")
                .antMatchers("/js/**")
                .antMatchers("/img/**");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        SsoInterceptor ssoInterceptor = new SsoInterceptor(tokenProvider, restTemplate, redirectUrl, includeUrls);

        http.addFilterBefore(new SsoFilter(ssoInterceptor), BasicAuthenticationFilter.class)
            .authorizeRequests()
                .antMatchers(includeUrls).authenticated();
    }
}

public class SsoInterceptor implements HandlerInterceptor {
    private TokenProvider tokenProvider;
    private RestTemplate restTemplate;
    private String redirectUrl;
    private String[] includeUrls;

    public SsoInterceptor(TokenProvider tokenProvider, RestTemplate restTemplate,
            String redirectUrl, String[] includeUrls) {
        this.tokenProvider = tokenProvider;
        this.restTemplate = restTemplate;
        this.redirectUrl = redirectUrl;
        this.includeUrls = includeUrls;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String token = CookieUtils.getCookie(request, "token");

        if (StringUtils.isEmpty(token) || !tokenProvider.validateToken(token)) {
            String returnUrl = request.getRequestURL().toString();
            response.sendRedirect(redirectUrl + "/login?returnUrl=" + URLEncoder.encode(returnUrl, "UTF-8"));
            return false;
        }

        String username = (String) cacheManager.getCache("tokens").get(token, String.class).get();
        if (StringUtils.isEmpty(username)) {
            response.sendRedirect(redirectUrl + "/login?error");
            return false;
        }

        // 设置用户身份信息
        List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER");
        User principal = new User(username, "", authorities);
        Authentication authentication = new UsernamePasswordAuthenticationToken(principal, "", authorities);
        SecurityContextHolder.getContext().setAuthentication(authentication);

        return true;
    }
}

public class SsoFilter extends OncePerRequestFilter {
    private SsoInterceptor ssoInterceptor;

    public SsoFilter(SsoInterceptor ssoInterceptor) {
        this.ssoInterceptor = ssoInterceptor;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        ssoInterceptor.preHandle(request, response, null);

        filterChain.doFilter(request, response);
    }
}

2.10 作为标识源发布令牌

最后,我们需要创建一个标识源项目,该项目用于将令牌发布给其他应用程序。在这个示例中,我们将运行标识源应用程序并发布Token,然后添加到其他应用程序的Cookie中。当用户从一个应用程序访问另一个应用程序时,应用程序将检查Token,并在用户已登录的情况下提供对应用程序的访问权限。

@RestController
public class SsoController {
    private TokenProvider tokenProvider;

    public SsoController(TokenProvider tokenProvider) {
        this.tokenProvider = tokenProvider;
    }

    @GetMapping("/singleSignOn/token")
    public String getToken(@RequestParam String username) {
        return tokenProvider.createToken(username);
    }
}

至此,我们已经成功实现了基于Spring Boot和Redis的单点登录。感谢阅读这篇攻略,希望它能够对您有所帮助!

本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:基于springboot和redis实现单点登录 - Python技术站

(0)
上一篇 2023年5月22日
下一篇 2023年5月22日

相关文章

  • oracle 临时表详解及实例

    Oracle 临时表详解及实例 什么是临时表 Oracle 临时表(Temporary Table),即只在当前会话中存在并可见,当会话结束时临时表数据将被自动清空。临时表可用于存储临时数据或中间结果,比如存储在子查询中生成的中间结果等。Oracle 临时表的表结构(表名、列名、数据类型、约束等)与普通表几乎一致,临时表支持的数据类型和约束也和普通表完全一致…

    database 2023年5月21日
    00
  • SQL删除语句DROP、TRUNCATE、 DELETE 的区别

    当我们使用SQL进行数据管理时,删除数据是一个非常关键的操作。这时候,就涉及到三种不同的删除语句:DROP、TRUNCATE以及DELETE。这三种语句的作用是相似的,但每一种语句的实现方式都不同。下面我们详细讲解一下这三种语句的区别。 DROP语句 DROP语句用于完全删除表(table)、视图(view)、索引(index)等数据库对象。执行DROP语句…

    database 2023年5月21日
    00
  • MySQL数据库的触发器的使用

    MySQL数据库的触发器是一种用于自动执行操作的机制。它可以捕获数据库中的事件,如INSERT、UPDATE和DELETE语句,然后自动执行相关操作。本篇文章将详细介绍MySQL数据库的触发器的使用。 什么是MySQL数据库的触发器? MySQL数据库的触发器是一种在表上创建的一种特殊类型的存储过程,它会在某些事件(如INSERT、UPDATE或DELETE…

    database 2023年5月22日
    00
  • 解决linux下redis数据库overcommit_memory问题

    让我来详细讲解解决linux下redis数据库overcommit_memory问题的完整攻略。 什么是overcommit_memory问题 在Linux系统下,overcommit_memory参数决定了操作系统在申请内存时是否进行系统调用,返回来确定操作系统是否可以保证可以提供申请的内存。而redis数据库在申请内存时,操作系统的处理和反馈消息使得re…

    database 2023年5月22日
    00
  • oracle表空间不足ORA-01653的问题: unable to extend table

    接下来我将为您讲解oracle表空间不足ORA-01653的问题,以下为完整攻略: 1. 什么是ORA-01653错误 在Oracle中,对于一些表的插入、更新或删除操作,可能会出现ORA-01653的错误,该错误提示的信息是”unable to extend table”,具有较为严重的影响。这是由于当前表空间的容量不足,Oracle无法再容纳新的数据而造…

    database 2023年5月21日
    00
  • 如何让tomcat服务增加java启动命令

    下面是详细的攻略: 前置条件 在开始配置Tomcat服务之前,需要确保已经按照官方文档正确安装了Tomcat,并且已经能够正常启动Tomcat服务。 步骤一:打开Tomcat服务配置文件 进入Tomcat安装目录下的bin文件夹,找到catalina.sh文件(Linux或MacOS)或catalina.bat文件(Windows)。这个文件用于配置Tomc…

    database 2023年5月22日
    00
  • mysql聚集索引、辅助索引、覆盖索引、联合索引的使用

    MySQL中索引是数据库优化的重要手段,常见的索引类型有聚集索引、辅助索引、覆盖索引和联合索引。 聚集索引 聚集索引也叫主键索引,是表中物理存储的排序方式,每个表只有一个聚集索引。聚集索引的叶子节点存储的是数据行本身而非指向数据的指针,因此可以减少一次IO开销。同时使用聚集索引的查询效率也会相应地提高。主键(primary key)约束自动创建聚集索引。 示…

    database 2023年5月22日
    00
  • MySQL按时间统计数据的方法总结

    MySQL按时间统计数据的方法总结 MySQL是一个常用的关系型数据库管理系统,常常需要按时间进行统计数据。本文总结了常见的按时间统计数据的方法。 方法一:使用DATE_FORMAT函数 SELECT DATE_FORMAT(created_at, ‘%Y-%m-%d’) AS day, COUNT(*) AS count FROM table GROUP …

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