接下来我将详细讲解“基于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技术站