no16-单点登录-session共享和jwt
分类: springboot 专栏: springboot学习 标签: 单点登录session共享
2023-04-11 00:16:58 1154浏览
单点登录
传统的登录(用户量不多的时候完全可以)
高并发的情况
这种集群的方式会涉及到一个session共享的问题。Tomcat1里的session存的登录信息不做处理的话,Tomcat2里是拿不到的。怎么解决这个问题呢?
方法一:可以把Tomcat配置成session广播共享的模式。这种情况的话适合集群节点不超过5个的时候。超过5个就会很耗费性能了,大家都在广播形成网络风暴,这种方案的好处就是不用改代码
方法二:采用分布式加单点登录的方式,用 redis解决session共享的问题
什么是单点登录
单点登录(Single Sign On),简称为 SSO,是比较流行的企业业务整合的解决方案之一。SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。
session共享
分布式存在session问题
测试controller
@RestController public class LoginController { @GetMapping("/login/{user}") public String login(@PathVariable String user, HttpSession session){ session.setAttribute("user",user); return user+"登录成功"; } @GetMapping("/getUser") public String getUser(HttpSession session){ return "当前登陆者:"+session.getAttribute("user"); } }
模拟集群
发现两个项目里的session无法共享
解决方案如下:
使用Redis解决Session共享问题的原理非常简单,就是把原本存储在不同服务器上的Session拿出来放在一个独立的服务器上。
虽然还是操作的HttpSession,但是实际上HttpSession容器已经被透明替换,真正的Session此时存储在Redis服务器上。
引入依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-data-redis</artifactId> </dependency>
启动类加注解
@EnableRedisHttpSession
测试session已经可以共享了。
jwt
什么是jwt
JSON Web Token(JWT)是一个开放的行业标准(RFC 7519),它定义了一种简洁的、自包含的协议格式,用于在通信双方传递json对象,传递的信息经过数字签名可以被验证和信任。JWT可以使用HMAC算法或使用RSA的公钥/私钥对来签名,防止被篡改。
JWT令牌的优点
1、jwt基于json,非常方便解析。
2、可以在令牌中自定义丰富的内容,易扩展。
3、通过非对称加密算法及数字签名技术,JWT防止篡改,安全性高。
4、资源服务使用JWT可不依赖认证服务即可完成授权。
令牌结构
JWT令牌由三部分组成,每部分中间使用点(.)分隔,比如:xxxxx.yyyyy.zzzzz
- Header
头部包括令牌的类型(即JWT)及使用的哈希算法(如HMAC SHA256或RSA)
一个例子如下:
下边是Header部分的内容
{ "alg": "HS256", "typ": "JWT" }
将上边的内容使用Base64Url编码,得到一个字符串就是JWT令牌的第一部分。
- Payload
第二部分是负载,内容也是一个json对象,它是存放有效信息的地方,它可以存放jwt提供的现成字段,比
如:iss(签发者),exp(过期时间戳), sub(面向的用户)等,也可自定义字段,比如用户角色。
此部分不建议存放敏感信息,因为此部分可以解码还原原始内容。
最后将第二部分负载使用Base64Url编码,得到一个字符串就是JWT令牌的第二部分。
一个例子:
{ "sub": "1234567890", "name": "456", "admin": true }
- Signature
第三部分是签名,此部分用于防止jwt内容被篡改。
这个部分使用base64url将前两部分进行编码,编码后使用点(.)连接组成字符串,最后使用header中声明
签名算法进行签名。
一个例子:
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
base64UrlEncode(header):jwt令牌的第一部分。
base64UrlEncode(payload):jwt令牌的第二部分。
secret:签名所使用的密钥。
springsecurity整合jwt
一般是在请求头里加入Authorization,并加Bearer标注 bearer [ˈbeərə(r)] 持票人; 送信人; 搬运工人;
Authorization Bearer eyJUWVAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJzZWN1cmUtYXBpIiwiYXVkIjoic2VjdXJlLWFwcCIsImV4cCI6MTYwNTQ4MzgwMCwic3ViIjoidXNlciIsImlhdCI6MTYwNTQ4Mzc0MCwicm9sIjpbIlJPTEVfVVNFUiJdfQ.vx0UseLZ62VMfacIrknkreMVVI0h7tIqGRYL5b3seHQ
依赖
<!--JWT 依赖--> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency> <!-- security依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
配置文件备用
#jwt存储的请求头 jwt.tokenHeader=Authorization #jwt加密使用的密钥 jwt.secret=jfit-secret #jwt的过期时间(60*60*24) jwt.expiration=604800 #jwt载荷中拿到开头 jwt.tokenHead=Bearer
jwt工具类备用
/** * JwtToken工具类 * * @author zhoubin * @since 1.0.0 */ @Component public class JwtTokenUtil { private static final String CLAIM_KEY_USERNAME = "sub"; private static final String CLAIM_KEY_CREATED = "created"; @Value("${jwt.secret}") private String secret; @Value("${jwt.expiration}") private Long expiration; /** * 根据用户信息生成token * * @param userDetails * @return */ public String generateToken(UserDetails userDetails) { Map<String, Object> claims = new HashMap<>(); claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername()); claims.put(CLAIM_KEY_CREATED, new Date()); return generateToken(claims); } /** * 从token中获取登录用户名 * @param token * @return */ public String getUserNameFromToken(String token){ String username; try { Claims claims = getClaimsFormToken(token); username = claims.getSubject(); } catch (Exception e) { username = null; } return username; } /** * 验证token是否有效 * @param token * @param userDetails * @return */ public boolean validateToken(String token,UserDetails userDetails){ String username = getUserNameFromToken(token); return username.equals(userDetails.getUsername()) && !isTokenExpired(token); } /** * 判断token是否可以被刷新 * @param token * @return */ public boolean canRefresh(String token){ return !isTokenExpired(token); } /** * 刷新token * @param token * @return */ public String refreshToken(String token){ Claims claims = getClaimsFormToken(token); claims.put(CLAIM_KEY_CREATED,new Date()); return generateToken(claims); } /** * 判断token是否失效 * @param token * @return */ private boolean isTokenExpired(String token) { Date expireDate = getExpiredDateFromToken(token); return expireDate.before(new Date()); } /** * 从token中获取过期时间 * @param token * @return */ private Date getExpiredDateFromToken(String token) { Claims claims = getClaimsFormToken(token); return claims.getExpiration(); } /** * 从token中获取荷载 * @param token * @return */ private Claims getClaimsFormToken(String token) { Claims claims = null; try { claims = Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); } catch (Exception e) { e.printStackTrace(); } return claims; } /** * 根据荷载生成JWT TOKEN * * @param claims * @return */ private String generateToken(Map<String, Object> claims) { return Jwts.builder() .setClaims(claims) .setExpiration(generateExpirationDate()) .signWith(SignatureAlgorithm.HS512, secret) .compact(); } /** * 生成token失效时间 * * @return */ private Date generateExpirationDate() { return new Date(System.currentTimeMillis() + expiration * 1000); } }
测试controller
@RestController public class HelloController { @GetMapping("/hello") public String hello() { return "hello jwt !"; } @GetMapping("/admin") public String admin() { return "hello admin !"; } }
spring security配置类
@Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Bean PasswordEncoder passwordEncoder() { return NoOpPasswordEncoder.getInstance(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication().withUser("admin") .password("123").roles("admin") .and() .withUser("wang") .password("456") .roles("user"); } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/hello").hasRole("user") .antMatchers("/admin").hasRole("admin") .antMatchers(HttpMethod.POST, "/login").permitAll() .anyRequest().authenticated() .and() .csrf().disable(); } }
实体类
public class User implements UserDetails { private String username; private String password; private List<GrantedAuthority> authorities; public String getUsername() { return username; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } public void setUsername(String username) { this.username = username; } @Override public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } @Override public List<GrantedAuthority> getAuthorities() { return authorities; } public void setAuthorities(List<GrantedAuthority> authorities) { this.authorities = authorities; } }
用户登录过滤器
public class JwtLoginFilter extends AbstractAuthenticationProcessingFilter { public JwtLoginFilter(String defaultFilterProcessesUrl, AuthenticationManager authenticationManager) { super(new AntPathRequestMatcher(defaultFilterProcessesUrl)); setAuthenticationManager(authenticationManager); } @Override public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse resp) throws AuthenticationException, IOException, ServletException { User user = new ObjectMapper().readValue(req.getInputStream(), User.class); return getAuthenticationManager().authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword())); } @Override protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse resp, FilterChain chain, Authentication authResult) throws IOException, ServletException { Collection<? extends GrantedAuthority> authorities = authResult.getAuthorities(); StringBuffer as = new StringBuffer(); for (GrantedAuthority authority : authorities) { as.append(authority.getAuthority()) .append(","); } String jwt = Jwts.builder() .claim("authorities", as)//配置用户角色 .setSubject(authResult.getName()) .setExpiration(new Date(System.currentTimeMillis() + 10 * 60 * 1000)) .signWith(SignatureAlgorithm.HS512,"sang@123") .compact(); resp.setContentType("application/json;charset=utf-8"); PrintWriter out = resp.getWriter(); out.write(new ObjectMapper().writeValueAsString(jwt)); out.flush(); out.close(); } protected void unsuccessfulAuthentication(HttpServletRequest req, HttpServletResponse resp, AuthenticationException failed) throws IOException, ServletException { resp.setContentType("application/json;charset=utf-8"); PrintWriter out = resp.getWriter(); out.write("登录失败!"); out.flush(); out.close(); } }
a 自定义 JwtLoginFilter 继承自AbstractAuthenticationProcessingFilter,并实现其中的三个默认方法。
b attemptAuthentication方法中,我们从登录参数中提取出用户名密码,然后调用AuthenticationManager.authenticate()方法去进行自动校验。
c 第二步如果校验成功,就会来到successfulAuthentication回调中,在successfulAuthentication方法中,将用户角色遍历然后用一个 , 连接起来,然后再利用Jwts去生成token,按照代码的顺序,生成过程一共配置了四个参数,分别是用户角色、主题、过期时间以及加密算法和密钥,然后将生成的token写出到客户端。
d 第二步如果校验失败就会来到unsuccessfulAuthentication方法中,在这个方法中返回一个错误提示给客户端即可。
token校验的过滤器
public class JwtFilter extends GenericFilterBean { @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) servletRequest; String jwtToken = req.getHeader("authorization"); if (StringUtils.hasText(jwtToken)) { System.out.println(jwtToken); Claims claims = Jwts.parser().setSigningKey("sang@123").parseClaimsJws(jwtToken.replace("Bearer","")) .getBody(); String username = claims.getSubject();//获取当前登录用户名 List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList((String) claims.get("authorities")); UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, null, authorities); SecurityContextHolder.getContext().setAuthentication(token); } filterChain.doFilter(req,servletResponse); } }
a 首先从请求头中提取出 authorization 字段,这个字段对应的value就是用户的token。
b 将提取出来的token字符串转换为一个Claims对象,再从Claims对象中提取出当前用户名和用户角色,创建一个UsernamePasswordAuthenticationToken放到当前 的Context中,然后执行过滤链使请求继续执行下去。
spring security配置类中配置过滤器
protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/hello").hasRole("user") .antMatchers("/admin").hasRole("admin") .antMatchers(HttpMethod.POST, "/login").permitAll() .anyRequest().authenticated() .and() .addFilterBefore(new JwtLoginFilter("/login",authenticationManager()), UsernamePasswordAuthenticationFilter.class) .addFilterBefore(new JwtFilter(),UsernamePasswordAuthenticationFilter.class) .csrf().disable(); }
测试
问题
- 1.在springsecurity整合jwt的时候是怎样校验token是否过期的,啥时候校验的。
答:会校验,登录的时候生成token的时候就已经设置了过期时间,后面请求保护的资源的时候回校验token的过期时间的,一旦过期就会抛出异常
- 2.在访问一个受保护的url的时候,未授权和未登录的自定义提示
@Component public class RestAuthorizationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { response.setCharacterEncoding("UTF-8"); response.setContentType("application/json"); PrintWriter out = response.getWriter(); RespBean bean = RespBean.error("尚未登录,请登录!"); bean.setCode(401); out.write(new ObjectMapper().writeValueAsString(bean)); out.flush(); out.close(); } }
@Component public class RestfulAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { response.setCharacterEncoding("UTF-8"); response.setContentType("application/json"); PrintWriter out = response.getWriter(); RespBean bean = RespBean.error("权限不足,请联系管理员!"); bean.setCode(403); out.write(new ObjectMapper().writeValueAsString(bean)); out.flush(); out.close(); } }
比较完整的security整合jwt的相关配置
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private IAdminService adminService; @Autowired private RestAuthorizationEntryPoint restAuthorizationEntryPoint; @Autowired private RestfulAccessDeniedHandler restfulAccessDeniedHandler; @Autowired private CustomFilter customFilter; @Autowired private CustomUrlDecisionManager customUrlDecisionManager; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder()); } @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers( "/login", "/logout", "/css/**", "/js/**", "/index.html", "favicon.ico", "/doc.html", "/webjars/**", "/swagger-resources/**", "/v2/api-docs/**", "/captcha", "/ws/**" ); } @Override protected void configure(HttpSecurity http) throws Exception { //使用JWT,不需要csrf http.csrf() .disable() //基于token,不需要session .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() //所有请求都要求认证 .anyRequest() .authenticated() //动态权限配置 .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() { @Override public <O extends FilterSecurityInterceptor> O postProcess(O object) { object.setAccessDecisionManager(customUrlDecisionManager); object.setSecurityMetadataSource(customFilter); return object; } }) .and() //禁用缓存 .headers() .cacheControl(); //添加jwt 登录授权过滤器 http.addFilterBefore(jwtAuthencationTokenFilter(), UsernamePasswordAuthenticationFilter.class); //添加自定义未授权和未登录结果返回 http.exceptionHandling() .accessDeniedHandler(restfulAccessDeniedHandler) .authenticationEntryPoint(restAuthorizationEntryPoint); } @Override @Bean public UserDetailsService userDetailsService(){ return username -> { Admin admin = adminService.getAdminByUserName(username); if (null!=admin){ admin.setRoles(adminService.getRoles(admin.getId())); return admin; } throw new UsernameNotFoundException("用户名或密码不正确"); }; } @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } @Bean public JwtAuthencationTokenFilter jwtAuthencationTokenFilter(){ return new JwtAuthencationTokenFilter(); } }
/** * JWT登录授权过滤器 * * @author zhoubin * @since 1.0.0 */ public class JwtAuthencationTokenFilter extends OncePerRequestFilter { @Value("${jwt.tokenHeader}") private String tokenHeader; @Value("${jwt.tokenHead}") private String tokenHead; @Autowired private JwtTokenUtil jwtTokenUtil; @Autowired private UserDetailsService userDetailsService; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String authHeader = request.getHeader(tokenHeader); //存在token if (null != authHeader && authHeader.startsWith(tokenHead)) { String authToken = authHeader.substring(tokenHead.length()); String username = jwtTokenUtil.getUserNameFromToken(authToken); //token存在用户名但未登录 if (null != username && null == SecurityContextHolder.getContext().getAuthentication()) { //登录 UserDetails userDetails = userDetailsService.loadUserByUsername(username); //验证token是否有效,重新设置用户对象 if (jwtTokenUtil.validateToken(authToken, userDetails)) { UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authenticationToken); } } } filterChain.doFilter(request, response); } }
登录的
@RestController public class LoginController { @Autowired private IAdminService adminService; @ApiOperation(value = "登录之后返回token") @PostMapping("/login") public RespBean login(@RequestBody AdminLoginParam adminLoginParam, HttpServletRequest request){ return adminService.login(adminLoginParam.getUsername(),adminLoginParam.getPassword(),adminLoginParam.getCode(),request); } @ApiOperation(value = "获取当前登录用户的信息") @GetMapping("/admin/info") public Admin getAdminInfo(Principal principal){ if (null==principal){ return null; } String username = principal.getName(); Admin admin = adminService.getAdminByUserName(username); admin.setPassword(null); admin.setRoles(adminService.getRoles(admin.getId())); return admin; } @ApiOperation(value = "退出登录") @PostMapping("/logout") public RespBean logout(){ return RespBean.success("注销成功!"); } }
好博客就要一起分享哦!分享海报