目录
1. 环境
2. 实现步骤
2.1 集成Redis
2.2 实现Vue登录页面
2.3 实现Spring Boot后端
2.3.1 用户认证
2.3.2 生成令牌
2.3.3 验证令牌
2.4 实现单点登录
2.4.1 登录流程
单点登录(Single Sign-On,SSO)是指在多个应用系统中,用户只需登录一次就可以访问多个应用系统的能力。下面将介绍如何使用Vue和Spring Boot实现单点登录。
1. 环境
- JDK 1.8+
- Spring Boot 2.x
- Vue.js 2.x
- Redis
2. 实现步骤
2.1 集成Redis
首先需要安装Redis以及Java的Redis客户端,这里使用Jedis作为Redis客户端。在Spring Boot项目中集成Redis,需要添加以下依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.3.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.3.0</version>
</dependency>
然后在application.properties中配置Redis连接信息:
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=
2.2 实现Vue登录页面
在Vue项目中,可以使用Axios发送登录请求,获取服务端返回的令牌,并将令牌保存到Cookie中:
// 登录
login() {
let params = new URLSearchParams();
params.append('username', this.username);
params.append('password', this.password);
axios.post('/api/login', params).then((response) => {
console.log(response);
if (response.data.code === 200) {
// 保存令牌
let token = response.data.token;
Cookie.set('token', token, { expires: 1 });
// 跳转到首页
this.$router.push('/home');
} else {
// 登录失败
this.$message.error(response.data.msg);
}
}).catch((error) => {
console.log(error);
});
}
// 登录
login() {
let params = new URLSearchParams();
params.append('username', this.username);
params.append('password', this.password);
axios.post('/api/login', params).then((response) => {
console.log(response);
if (response.data.code === 200) {
// 保存令牌
let token = response.data.token;
Cookie.set('token', token, { expires: 1 });
// 跳转到首页
this.$router.push('/home');
} else {
// 登录失败
this.$message.error(response.data.msg);
}
}).catch((error) => {
console.log(error);
});
}
2.3 实现Spring Boot后端
服务端使用Spring Boot实现单点登录,需要实现以下几个步骤:
2.3.1 用户认证
用户登录后,服务端需要进行用户认证。这里使用Spring Security进行用户认证,需要添加以下依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
然后在WebSecurityConfigurerAdapter中进行配置:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Autowired
private PasswordEncoder passwordEncoder;
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().authorizeRequests()
.antMatchers("/api/login").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginProcessingUrl("/api/login").permitAll()
.successHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
out.write("{\"code\":200,\"msg\":\"登录成功\"}");
out.flush();
out.close();
}
})
.failureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
out.write("{\"code\":401,\"msg\":\"" + exception.getMessage() + "\"}");
out.flush();
out.close();
}
})
.and()
.logout()
.logoutSuccessHandler(new LogoutSuccessHandler() {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
out.write("{\"code\":200,\"msg\":\"退出登录成功\"}");
out.flush();
out.close();
}
})
.deleteCookies("JSESSIONID", "SESSION");
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
}
}
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Autowired
private PasswordEncoder passwordEncoder;
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().authorizeRequests()
.antMatchers("/api/login").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginProcessingUrl("/api/login").permitAll()
.successHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
out.write("{\"code\":200,\"msg\":\"登录成功\"}");
out.flush();
out.close();
}
})
.failureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
out.write("{\"code\":401,\"msg\":\"" + exception.getMessage() + "\"}");
out.flush();
out.close();
}
})
.and()
.logout()
.logoutSuccessHandler(new LogoutSuccessHandler() {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
out.write("{\"code\":200,\"msg\":\"退出登录成功\"}");
out.flush();
out.close();
}
})
.deleteCookies("JSESSIONID", "SESSION");
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
}
}
其中,UserDetailsServiceImpl实现UserDetailsService接口,从数据库中获取用户信息:
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserService userService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userService.getUserByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("用户不存在");
}
List<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList(user.getRoles().split(","));
return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), authorities);
}
}
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserService userService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userService.getUserByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("用户不存在");
}
List<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList(user.getRoles().split(","));
return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), authorities);
}
}
PasswordEncoder使用BCryptPasswordEncoder,对用户密码进行加密:
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
2.3.2 生成令牌
用户登录成功后,服务端需要生成令牌,并将令牌存储到Redis中。这里使用JWT作为令牌生成工具,需要添加以下依赖:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
然后在JwtUtil中实现令牌生成方法:
public String generateToken(String username) {
Date now = new Date();
Date expiration = new Date(now.getTime() + expirationTime * 1000);
return Jwts.builder()
.setClaims(new HashMap<>())
.setSubject(username)
.setIssuedAt(now)
.setExpiration(expiration)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
public String generateToken(String username) {
Date now = new Date();
Date expiration = new Date(now.getTime() + expirationTime * 1000);
return Jwts.builder()
.setClaims(new HashMap<>())
.setSubject(username)
.setIssuedAt(now)
.setExpiration(expiration)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
2.3.3 验证令牌
用户访问其他应用系统时,需要验证用户是否已登录,这里使用Filter实现令牌验证。定义一个AuthFilter,实现doFilter方法:
public class AuthFilter extends OncePerRequestFilter {
private static final String AUTH_HEADER = "Authorization";
@Autowired
private JwtUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String authHeader = request.getHeader(AUTH_HEADER);
if (StringUtils.isNotBlank(authHeader) && authHeader.startsWith("Bearer ")) {
String token = authHeader.substring(7);
Claims claims = null;
try {
claims = jwtUtil.parseToken(token);
} catch (Exception e) {
e.printStackTrace();
}
if (claims != null && claims.getExpiration().after(new Date())) {
String username = claims.getSubject();
// 验证用户是否已登录
if (StringUtils.isNotBlank(username) && JedisUtil.exists(username)) {
filterChain.doFilter(request, response);
return;
}
}
}
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
out.write("{\"code\":401,\"msg\":\"请先登录\"}");
out.flush();
out.close();
}}
public class AuthFilter extends OncePerRequestFilter {
private static final String AUTH_HEADER = "Authorization";
@Autowired
private JwtUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String authHeader = request.getHeader(AUTH_HEADER);
if (StringUtils.isNotBlank(authHeader) && authHeader.startsWith("Bearer ")) {
String token = authHeader.substring(7);
Claims claims = null;
try {
claims = jwtUtil.parseToken(token);
} catch (Exception e) {
e.printStackTrace();
}
if (claims != null && claims.getExpiration().after(new Date())) {
String username = claims.getSubject();
// 验证用户是否已登录
if (StringUtils.isNotBlank(username) && JedisUtil.exists(username)) {
filterChain.doFilter(request, response);
return;
}
}
}
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
out.write("{\"code\":401,\"msg\":\"请先登录\"}");
out.flush();
out.close();
}}
然后在WebSecurityConfigurerAdapter中添加AuthFilter:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private AuthFilter authFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().authorizeRequests()
.antMatchers("/api/login").permitAll()
.anyRequest().authenticated()
.and()
.addFilterBefore(authFilter, UsernamePasswordAuthenticationFilter.class)
// ...
}
}
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private AuthFilter authFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().authorizeRequests()
.antMatchers("/api/login").permitAll()
.anyRequest().authenticated()
.and()
.addFilterBefore(authFilter, UsernamePasswordAuthenticationFilter.class)
// ...
}
}
2.4 实现单点登录
实现了用户认证、生成令牌和验证令牌之后,就可以实现单点登录了。
2.4.1 登录流程
用户在一个应用系统中登录,然后访问其他应用系统时,其他应用系统需要验证用户是否已登录。这里的步骤如下:
- 用户在第一个应用系统中登录,该系统使用单点登录技术生成一个令牌,并将该令牌返回给用户。
- 用户在访问其他应用系统时,在 HTTP 请求中添加该令牌。
- 接收请求的应用系统将该令牌发送到单点登录服务器进行验证。
- 单点登录服务器验证该令牌,如果验证通过,则说明用户已登录,应用系统可以允许用户访问该系统的资源。
- 如果验证未通过,则说明用户未登录或者登录已过期,应用系统需要引导用户重新登录或者跳转到登录页面。
- 用户在其他应用系统中的操作会触发新的 HTTP 请求,其中仍然需要携带该令牌进行身份验证。