单点登录实现方案:
①SpringBoot整合SpringSecurity+JWT实现单点登录
- ②SpringBoot+OAuth2+SpringSecurity+JWT实现单点登录
- ③Springboot整合shiro+jwt实现单点登录
①SpringBoot整合SpringSecurity+JWT实现单点登录方案:
1.单点登录的概念
Single Sign On(简称SSO)。业务初始,我们所有的功能都在一个系统,比如登录系统、订单系统、购物车系统都在一台机子,使用的都是同一台机器的登录系统。随着业务的发展,业务进行了拆分,分割出多个业务系统,分别部署在不同的机子上,那用户的登录信息只在其中一台机子上面,要想获取用户信息,就得每次都登录一遍登录系统,那就很繁琐,无论是性能还是用户体验都不是最佳方案。所以,后面演化出了SSO,简单来说,单点登录就是在多个系统中,用户只需一次登录,各个系统即可感知该用户已经登录。
2.多系统登录的问题解决
众所周知,HTTP是无状态的协议,这意味着服务器无法确认用户的信息。于是乎:给每一个用户都发一个通行证,无论谁访问的时候都需要携带通行证,这样服务器就可以从通行证上确认用户的信息,这个通行证我们理解为token,怎么做到无状态和跨域,我们后面使用的技术是JWT。
3.session实现的单点登录问题
4.实现方式及效果
用户不带token访问系统B,系统B响应状态码401(需要认证)
用户登录系统A,系统A校验用户名密码成功,生成并响应token及状态码200
用户没有登录系统B而是携带系统A响应的token去访问系统B
系统B解析token并进行权限校验,如果系统A的token解析出来发现权限不足访问资源则响应403,权限验证成功则响应正常的json数据
访问系统C、系统D或分布式集群亦是如此。
5.认证思路分析
用户认证:
由于分布式项目,多数是前后端分离的架构设计,我们要满足可以接受异步post的认证请求参数,需要修改UsernamePasswordAuthenticationFilter过滤器中attemptAuthentication方法,让其能够接收请求体。
另外,默认successfulAuthentication方法在认证通过后,是把用户信息直接放入session就完事了,现在我们需要修改这个方法,在认证通过后生成token并返回给用户。
身份校验:
原来BasicAuthenticationFilter过滤器中doFilterInternal方法校验用户是否登录,就是看session中是否有用户信息,我们要修改为,验证用户携带的token是否合法,并解析出用户信息,交给SpringSecurity,以便于后续的授权功能可以正常使用。
6.过滤器和登录拦截全局配置
我们实现单点登录配置了两个过滤器类和一个登录拦截全局配置:
①UserAuthenticationAndGeneralToeknFilter:
这个过滤器的实现以及主要工作:
1.过滤器实现:
extends UsernamePasswordAuthenticationFilter类,重写attemptAuthentication(…)、unsuccessfulAuthentication(…)、successfulAuthentication(…)方法。
2.主要工作:
2.1设置了登录请求拦截,拦截登录接口请求
public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
// 登录请求拦截
// SecurityConstants.AUTH_LOGIN_URL="/user/login";
setFilterProcessesUrl(SecurityConstants.AUTH_LOGIN_URL);
}
2.2拦截登录请求的用户名和密码,安全框架进行用户名和密码认证
/**
* 用户信息认证
*
* @param request 请求体
* @param response 相应体
* @return Authentication
* @throws AuthenticationException 认证异常
*/
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
try {
// 进行用户名和密码认证-登录拦截全局配置类重写的protected UserDetailsService userDetailsService()的loadUserByUsername方法,可以进行自定义逻辑的用户信息认证,具体的实现在loadUserByUsername方法,这里是讲的一个拦截处理的流程
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(request.getParameter("userId"), request.getParameter("pwd"));
return authenticationManager.authenticate(authRequest);
} catch (Exception e) {
// 认证异常我直接处理了,因为我发现当用户不存在的时候我们抛出来的是UsernameNotFoundException,但是unsuccessfulAuthentication接收到的异常却是BadCredentialsException(这涉及到一个属性配置,我配置了半天没搞通,放弃了),总是提示 用户名密码不正确,解决的办法就是可以自定义异常或者直接在这里捕获处理都行
try {
response.setContentType("application/json;charset=utf-8");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
PrintWriter out = response.getWriter();
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("code", HttpServletResponse.SC_FORBIDDEN);
resultMap.put("msg", e.getMessage());
out.write(new ObjectMapper().writeValueAsString(resultMap));
out.flush();
out.close();
} catch (Exception outEx) {
throw new RuntimeException(outEx);
}
}
return null;
}
2.3安全框架用户名和密码认证异常和成功的处理
异常自定义回写到header:
/**
* 认证失败异常捕捉返回-异常是安全配置类loadUserByUsername方法自定义抛出的
*
* @param request 请求体
* @param response 相应体
* @param failed 异常
*/
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) {
// //清理上下文
// SecurityContextHolder.clearContext();
// //判断异常类
// if (failed instanceof InternalAuthenticationServiceException) {
// write(response, HttpServletResponse.SC_FORBIDDEN, "认证服务不正常!");
// } else if (failed instanceof UsernameNotFoundException) {
// write(response, HttpServletResponse.SC_FORBIDDEN, "用户账户不存在!");
// } else if (failed instanceof BadCredentialsException) {
// write(response, HttpServletResponse.SC_FORBIDDEN, "用户密码是错的!");
// } else if (failed instanceof AccountExpiredException) {
// write(response, HttpServletResponse.SC_FORBIDDEN, "用户账户已过期!");
// } else if (failed instanceof LockedException) {
// write(response, HttpServletResponse.SC_FORBIDDEN, "用户账户已被锁!");
// } else if (failed instanceof CredentialsExpiredException) {
// write(response, HttpServletResponse.SC_FORBIDDEN, "用户密码已失效!");
// } else if (failed instanceof DisabledException) {
// write(response, HttpServletResponse.SC_FORBIDDEN, "用户账户已被锁!");
// }
}
认证成功:生成token,回写到header:
/**
* 认证成功生成token
*
* @param request 请求体
* @param response 响应体
* @param chain 过滤器链
* @param authResult 认证信息
* @throws IOException IO异常
*/
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authResult) throws IOException {
// 用户id
String userId = request.getParameter("userId");
// 用户角色
UserDetails user = (UserDetails) authResult.getPrincipal();
List<?> roles = user.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList());
// 加盐字符串
byte[] signingKey = SecurityConstants.JWT_SECRET.getBytes();
// token过期时间 每次登录成功续期864000000
long expireTime = System.currentTimeMillis() + 864000000;
Date expireDate = new Date(expireTime);
// token生成
String token = Jwts.builder()
.signWith(Keys.hmacShaKeyFor(signingKey), SignatureAlgorithm.HS512)
.setHeaderParam("typ", SecurityConstants.TOKEN_TYPE) // JWT类型:jwt
.setIssuer(SecurityConstants.TOKEN_ISSUER) // 放行方:字符串secure-api
.setAudience(SecurityConstants.TOKEN_AUDIENCE) // 字符串secure-app
.setSubject(user.getUsername())
.setExpiration(expireDate)
.claim("rol", roles)
.compact();
// 设置浏览器的header,设置token
response.addHeader(SecurityConstants.TOKEN_HEADER, SecurityConstants.TOKEN_PREFIX + token);
// 相关信息返回
Map<String, Object> content = new HashMap<String, Object>();
content.put("token", token);
content.put("userId", userId);
FMResponse fm = new FMResponse(1, "用户认证通过", content);
String FmJson = JSON.toJSONString(fm, SerializerFeature.WriteMapNullValue);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().append(FmJson);
}
好了,到这里我们第一个过滤器的工作流程就记录完了,主要工作是用户认证和认证之后异常处理或者成功之后的token生成处理。至于这个过滤器的使用,则是在登录拦截全局配置中使用,这个看配置就知道了。
完整代码:
/**
* 用户名密码认证过滤器
*/
public class UserAuthenticationAndGeneralToeknFilter extends UsernamePasswordAuthenticationFilter {
Logger logger = LoggerFactory.getLogger(UserAuthenticationAndGeneralToeknFilter.class);
private final AuthenticationManager authenticationManager;
public UserAuthenticationAndGeneralToeknFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
setFilterProcessesUrl(SecurityConstants.AUTH_LOGIN_URL);
}
private UserLoginFailRepository userLoginFailureRepository() {
return SpringContext.getBean(UserLoginFailRepository.class);
}
private UserVerifiedCodeRepository userVerifiedCodeRepository() {
return SpringContext.getBean(UserVerifiedCodeRepository.class);
}
private UserRepository userRepository() {
return SpringContext.getBean(UserRepository.class);
}
/**
* 用户信息认证
*
* @param request 请求体
* @param response 相应体
* @return Authentication
* @throws AuthenticationException 认证异常
*/
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
try {
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(request.getParameter("userId"), request.getParameter("pwd"));
return authenticationManager.authenticate(authRequest);
} catch (Exception e) {
try {
response.setContentType("application/json;charset=utf-8");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
PrintWriter out = response.getWriter();
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("code", HttpServletResponse.SC_FORBIDDEN);
resultMap.put("msg", e.getMessage());
out.write(new ObjectMapper().writeValueAsString(resultMap));
out.flush();
out.close();
} catch (Exception outEx) {
throw new RuntimeException(outEx);
}
// throw new RuntimeException(e);
}
return null;
}
/**
* 认证失败异常捕捉返回-异常是安全配置类loadUserByUsername方法自定义抛出的
*
* @param request 请求体
* @param response 相应体
* @param failed 异常
*/
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) {
// //清理上下文
// SecurityContextHolder.clearContext();
// //判断异常类
// if (failed instanceof InternalAuthenticationServiceException) {
// write(response, HttpServletResponse.SC_FORBIDDEN, "认证服务不正常!");
// } else if (failed instanceof UsernameNotFoundException) {
// write(response, HttpServletResponse.SC_FORBIDDEN, "用户账户不存在!");
// } else if (failed instanceof BadCredentialsException) {
// write(response, HttpServletResponse.SC_FORBIDDEN, "用户密码是错的!");
// } else if (failed instanceof AccountExpiredException) {
// write(response, HttpServletResponse.SC_FORBIDDEN, "用户账户已过期!");
// } else if (failed instanceof LockedException) {
// write(response, HttpServletResponse.SC_FORBIDDEN, "用户账户已被锁!");
// } else if (failed instanceof CredentialsExpiredException) {
// write(response, HttpServletResponse.SC_FORBIDDEN, "用户密码已失效!");
// } else if (failed instanceof DisabledException) {
// write(response, HttpServletResponse.SC_FORBIDDEN, "用户账户已被锁!");
// }
}
/**
* 向浏览器响应一个json字符串
*
* @param response 响应对象
* @param code 状态码
* @param msg 响应信息
*/
public void write(HttpServletResponse response, int code, String msg) {
try {
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Cache-Control", "no-cache");
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
response.setStatus(code);
PrintWriter out = response.getWriter();
Map<String, Object> map = new HashMap<>();
map.put("code", code);
map.put("msg", msg);
out.write(JsonUtils.toJson(map));
out.flush();
out.close();
} catch (Exception e) {
logger.error("响应出错:" + msg, e);
}
}
/**
* 认证成功生成token
*
* @param request 请求体
* @param response 响应体
* @param chain 过滤器链
* @param authResult 认证信息
* @throws IOException IO异常
*/
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authResult) throws IOException {
// 用户id
String userId = request.getParameter("userId");
// 用户角色
UserDetails user = (UserDetails) authResult.getPrincipal();
List<?> roles = user.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList());
// 加盐字符串
byte[] signingKey = SecurityConstants.JWT_SECRET.getBytes();
// token过期时间 每次登录成功续期864000000
long expireTime = System.currentTimeMillis() + 864000000;
Date expireDate = new Date(expireTime);
// token生成
String token = Jwts.builder()
.signWith(Keys.hmacShaKeyFor(signingKey), SignatureAlgorithm.HS512)
.setHeaderParam("typ", SecurityConstants.TOKEN_TYPE) // JWT类型:jwt
.setIssuer(SecurityConstants.TOKEN_ISSUER) // 放行方:字符串secure-api
.setAudience(SecurityConstants.TOKEN_AUDIENCE) // 字符串secure-app
.setSubject(user.getUsername())
.setExpiration(expireDate)
.claim("rol", roles)
.compact();
// 设置浏览器的header,设置token
response.addHeader(SecurityConstants.TOKEN_HEADER, SecurityConstants.TOKEN_PREFIX + token);
// 相关信息返回
Map<String, Object> content = new HashMap<String, Object>();
content.put("token", token);
content.put("userId", userId);
FMResponse fm = new FMResponse(1, "用户认证通过", content);
String FmJson = JSON.toJSONString(fm, SerializerFeature.WriteMapNullValue);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().append(FmJson);
}
}
②DoFilterInternalAndJwtToeknParseFilter
这个过滤器的实现以及主要工作:非登录请求拦截,验证请求携带的token的合法性。
1.过滤器实现:extends BasicAuthenticationFilter,重写doFilterInternal()和getAuthentication()方法。
/**
* 非登录请求拦截
*
* @param request 请求体
* @param response 响应体
* @param chain 过滤器链
* @throws IOException IO异常
* @throws ServletException 异常
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 验证token的合法性
UsernamePasswordAuthenticationToken authenticationToken = getAuthentication(request);
if (authenticationToken == null) {
chain.doFilter(request, response);
return;
}
// 如果用户被删除,阻止其使用。
String userId = authenticationToken.getName();
Optional<RemovedUserEntity> userEntityOptional = removedUserRepository().findByUserId(userId);
if (userEntityOptional.isPresent()) {
return;
}
// 放置到SecurityContextHolder,这样便完成了springsecurity和jwt的整合
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
// 校验成功,准备分发请求到Servlet
chain.doFilter(request, response);
}
/**
* token认证
*
* @param request 请求体
* @return UsernamePasswordAuthenticationToken
*/
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
String token = request.getHeader(com.hermes.config.SecurityConstants.TOKEN_HEADER);
if (!StringUtils.isEmpty(token) && token.startsWith(com.hermes.config.SecurityConstants.TOKEN_PREFIX)) {
try {
byte[] signingKey = com.hermes.config.SecurityConstants.JWT_SECRET.getBytes();
Jws<Claims> parsedToken = Jwts
.parserBuilder().setSigningKey(signingKey).build()
.parseClaimsJws(token.replace("Bearer ", ""));
String username = parsedToken.getBody().getSubject();
Collection<? extends GrantedAuthority> authorities = ((List<?>) parsedToken.getBody().get("rol"))
.stream()
.map(authority -> new SimpleGrantedAuthority((String) authority)).collect(Collectors.toList());
if (!StringUtils.isEmpty(username)) {
return new UsernamePasswordAuthenticationToken(username, null, authorities);
}
} catch (ExpiredJwtException exception) {
log.error("Request to parse expired JWT : {} failed : {}", token, exception.getMessage());
} catch (UnsupportedJwtException exception) {
log.error("Request to parse unsupported JWT : {} failed : {}", token, exception.getMessage());
} catch (MalformedJwtException exception) {
log.error("Request to parse invalid JWT : {} failed : {}", token, exception.getMessage());
} catch (SignatureException exception) {
log.error("Request to parse JWT with invalid signature : {} failed : {}", token, exception.getMessage());
} catch (IllegalArgumentException exception) {
log.error("Request to parse empty or null JWT : {} failed : {}", token, exception.getMessage());
}
}
return null;
}
好了,第二个过滤器也搞定了,针对的非登录请求的token合法性验证。
完整代码:
public class DoFilterInternalAndJwtToeknParseFilter extends BasicAuthenticationFilter {
Logger log = LoggerFactory.getLogger(DoFilterInternalAndJwtToeknParseFilter.class);
public DoFilterInternalAndJwtToeknParseFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
private RemovedUserRepository removedUserRepository() {
return SpringContext.getBean(RemovedUserRepository.class);
}
/**
* 非登录请求拦截
*
* @param request 请求体
* @param response 响应体
* @param chain 过滤器链
* @throws IOException IO异常
* @throws ServletException 异常
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 验证token的合法性
UsernamePasswordAuthenticationToken authenticationToken = getAuthentication(request);
if (authenticationToken == null) {
chain.doFilter(request, response);
return;
}
// 如果用户被删除,阻止其使用。
String userId = authenticationToken.getName();
Optional<RemovedUserEntity> userEntityOptional = removedUserRepository().findByUserId(userId);
if (userEntityOptional.isPresent()) {
return;
}
// 放置到SecurityContextHolder,这样便完成了springsecurity和jwt的整合
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
// 校验成功,准备分发请求到Servlet
chain.doFilter(request, response);
}
/**
* token认证
*
* @param request 请求体
* @return UsernamePasswordAuthenticationToken
*/
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) throws ExpiredJwtException {
String token = request.getHeader(com.hermes.config.SecurityConstants.TOKEN_HEADER);
if (!StringUtils.isEmpty(token) && token.startsWith(com.hermes.config.SecurityConstants.TOKEN_PREFIX)) {
try {
byte[] signingKey = com.hermes.config.SecurityConstants.JWT_SECRET.getBytes();
Jws<Claims> parsedToken = Jwts
.parserBuilder().setSigningKey(signingKey).build()
.parseClaimsJws(token.replace("Bearer ", ""));
String username = parsedToken.getBody().getSubject();
Collection<? extends GrantedAuthority> authorities = ((List<?>) parsedToken.getBody().get("rol"))
.stream()
.map(authority -> new SimpleGrantedAuthority((String) authority)).collect(Collectors.toList());
if (!StringUtils.isEmpty(username)) {
return new UsernamePasswordAuthenticationToken(username, null, authorities);
}
}
catch (ExpiredJwtException exception) {
throw new ExpiredJwtException(null,null,exception.getMessage());
} catch (UnsupportedJwtException exception) {
throw new UnsupportedJwtException(exception.getMessage());
} catch (MalformedJwtException exception) {
throw new MalformedJwtException(exception.getMessage());
} catch (SignatureException exception) {
throw new SignatureException(exception.getMessage());
} catch (IllegalArgumentException exception) {
throw new IllegalArgumentException(exception.getMessage());
}
}
return null;
}
③登录拦截全局配置
主要工作:
配置放行和拦截接口
添加两个过滤器addFilter()
loadUserByUsername():登录接口的用户信息认证:比如:用户账户不存在,用户密码错误等相关验证校验,错误信息通过响应的异常抛出。
完整代码:
/**
* 登录拦截全局配置
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
UserVerifiedCodeRepository userVerifiedCodeRepository;
@Autowired
UserRepository userRepository;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors()
.and()
.csrf().disable() // 关闭csrf验证(防止跨站请求伪造攻击)
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/user/register").permitAll() // 注册请求放行
// .antMatchers(HttpMethod.POST, "/user/logout").permitAll()
// .antMatchers(HttpMethod.GET,"/historySend/findHistorySendBusinessTypes").permitAll()
.anyRequest().authenticated() // 其他请求统统拦截进行身份认证
.and()
.addFilter(new com.h.config.UserAuthenticationAndGeneralToeknFilter(authenticationManager())) // 自定义认证过滤器
.addFilter(new com.h.config.DoFilterInternalAndJwtToeknParseFilter(authenticationManager())) // token认证器
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); // session 无状态
}
@Bean
@Override
protected UserDetailsService userDetailsService() {
return new UserDetailsService() {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 业务校验逻辑,具体的业务场景具体实现
Optional<UserInfo> userInfo = userRepository.findByUserId(username);
if (!userInfo.isPresent()){
throw new BadCredentialsException(username + "用户不存在,登录失败");
}
Optional<UserVerifiedCode> codeOptional = userVerifiedCodeRepository.findById(username);
if (!codeOptional.isPresent()){
throw new BadCredentialsException(username + "验证码为空,登录失败");
}
String role = "USER";
if(userInfo.get().getRole() != null){
role = userInfo.get().getRole();
}
return User.withUsername(username)
.password(passwordEncoder().encode(codeOptional.get().getCode()))
.roles(role) .build();
}
};
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowedOrigins(Collections.singletonList("*"));
corsConfiguration.setAllowedMethods(Collections.singletonList("*"));
corsConfiguration.addExposedHeader("LoginFailed");
corsConfiguration.addAllowedHeader("LoginFailed");
corsConfiguration.addExposedHeader("authorization");
corsConfiguration.addAllowedHeader("authorization");
corsConfiguration.setAllowedHeaders(Collections.singletonList("*"));
corsConfiguration.addExposedHeader("Content-Disposition");
corsConfiguration.addAllowedHeader("Content-Disposition");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", corsConfiguration);
return source;
}
}
7.引入的依赖
implementation ‘org.springframework.boot:spring-boot-starter-security’
implementation ‘io.jsonwebtoken:jjwt-api:0.11.1’
implementation ‘io.jsonwebtoken:jjwt-impl:0.11.1’
implementation ‘io.jsonwebtoken:jjwt-jackson:0.11.1’