暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

spring security

原创 等什么君 2021-05-20
717

spring security

1 连接数据库查询

1.1 自定义service
@Service public class UserService implements UserDetailsService { @Autowired UserDao userDao; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userDao.findUserByUsername(username); if (user == null) { throw new UsernameNotFoundException("用户不存在"); } return user; } }
1.2 在SecuityConfig配置类中注入自定义service
@Autowired UserService userService; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userService); }

2 登录流程

2.1 UsernamePasswordAuthenticationFilter
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter { public UsernamePasswordAuthenticationFilter() { super(new AntPathRequestMatcher("/login", "POST")); } public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { String username = obtainUsername(request); String password = obtainPassword(request); UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken( username, password); setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); } protected String obtainPassword(HttpServletRequest request) { return request.getParameter(passwordParameter); } protected String obtainUsername(HttpServletRequest request) { return request.getParameter(usernameParameter); } protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) { authRequest.setDetails(authenticationDetailsSource.buildDetails(request)); } }
2.2 ProviderManager的authenticate()方法
public Authentication authenticate(Authentication authentication) throws AuthenticationException { Class<? extends Authentication> toTest = authentication.getClass(); for (AuthenticationProvider provider : getProviders()) { if (!provider.supports(toTest)) { continue; } result = provider.authenticate(authentication); if (result != null) { copyDetails(authentication, result); break; } } if (result == null && parent != null) { result = parentResult = parent.authenticate(authentication); } if (result != null) { if (eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) { ((CredentialsContainer) result).eraseCredentials(); } if (parentResult == null) { eventPublisher.publishAuthenticationSuccess(result); } return result; } throw lastException; }

parent 就是 ProviderManager,所以会再次回到这个 authenticate 方法中。再次回到 authenticate 方法中,provider 也变成了 DaoAuthenticationProvider,这个 provider 是支持 UsernamePasswordAuthenticationToken 的,所以会顺利进入到该类的 authenticate 方法去执行,而 DaoAuthenticationProvider 继承自 AbstractUserDetailsAuthenticationProvider 并且没有重写 authenticate 方法,所以 我们最终来到 AbstractUserDetailsAuthenticationProvider#authenticate 方法中

2.3 AbstractUserDetailsAuthenticationProvider#authenticate
public Authentication authenticate(Authentication authentication) throws AuthenticationException { String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName(); user = retrieveUser(username,(UsernamePasswordAuthenticationToken) authentication); preAuthenticationChecks.check(user); additionalAuthenticationChecks(user,(UsernamePasswordAuthenticationToken) authentication); postAuthenticationChecks.check(user); Object principalToReturn = user; if (forcePrincipalAsString) { principalToReturn = user.getUsername(); } return createSuccessAuthentication(principalToReturn, authentication, user); }
2.4 DaoAuthenticationProvider#additionalAuthenticationChecks
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { if (authentication.getCredentials() == null) { logger.debug("Authentication failed: no credentials provided"); throw new BadCredentialsException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } String presentedPassword = authentication.getCredentials().toString(); if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) { logger.debug("Authentication failed: password does not match stored value"); throw new BadCredentialsException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } }
2.5 AbstractAuthenticationProcessingFilter#doFilter
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; Authentication authResult; try { authResult = attemptAuthentication(request, response); if (authResult == null) { return; } sessionStrategy.onAuthentication(authResult, request, response); } catch (InternalAuthenticationServiceException failed) { unsuccessfulAuthentication(request, response, failed); return; } catch (AuthenticationException failed) { unsuccessfulAuthentication(request, response, failed); return; } if (continueChainBeforeSuccessfulAuthentication) { chain.doFilter(request, response); } successfulAuthentication(request, response, chain, authResult); }

毫无疑问,UsernamePasswordAuthenticationFilter#attemptAuthentication 方法就是在 AbstractAuthenticationProcessingFilter 类的 doFilter 方法中被触发的,成功会调用successfulAuthentication,失败则调用unsuccessfulAuthentication。

2.6 AbstractAuthenticationProcessingFilter#successfulAuthentication
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { SecurityContextHolder.getContext().setAuthentication(authResult); rememberMeServices.loginSuccess(request, response, authResult); // Fire event if (this.eventPublisher != null) { eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent( authResult, this.getClass())); } successHandler.onAuthenticationSuccess(request, response, authResult); }

在这里有一段很重要的代码,就是 SecurityContextHolder.getContext().setAuthentication(authResult); ,登录成功的用户信息被保存在这里,也就是说,在任何地方,如果我们想获取用户登录信息,都可以从 SecurityContextHolder.getContext() 中获取到,想修改,也可以在这里修改。

2.7 successHandler.onAuthenticationSuccess(request, response, authResult)最后这个方法是在SecurityConfig类中自行配置的

3 登录实现json类型传参

3.1 自定义一个过滤器代替 UsernamePasswordAuthenticationFilter
public class LoginFilter extends UsernamePasswordAuthenticationFilter { @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { if (!request.getMethod().equals("POST")) { throw new AuthenticationServiceException( "Authentication method not supported: " + request.getMethod()); } String verify_code = (String) request.getSession().getAttribute("verify_code"); if (request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE) || request.getContentType().equals(MediaType.APPLICATION_JSON_UTF8_VALUE)) { Map<String, String> loginData = new HashMap<>(); try { loginData = new ObjectMapper().readValue(request.getInputStream(), Map.class); } catch (IOException e) { }finally { String code = loginData.get("code"); checkCode(response, code, verify_code); } String username = loginData.get(getUsernameParameter()); String password = loginData.get(getPasswordParameter()); if (username == null) { username = ""; } if (password == null) { password = ""; } username = username.trim(); UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken( username, password); setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); } else { checkCode(response, request.getParameter("code"), verify_code); return super.attemptAuthentication(request, response); } } public void checkCode(HttpServletResponse resp, String code, String verify_code) { if (code == null || verify_code == null || "".equals(code) || !verify_code.toLowerCase().equals(code.toLowerCase())) { //验证码不正确 throw new AuthenticationServiceException("验证码不正确"); } } }
3.2 提供自定义filter的实例
@Bean LoginFilter loginFilter() throws Exception { LoginFilter loginFilter = new LoginFilter(); loginFilter.setAuthenticationSuccessHandler(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(); Hr hr = (Hr) authentication.getPrincipal(); hr.setPassword(null); RespBean ok = RespBean.ok("登录成功!", hr); String s = new ObjectMapper().writeValueAsString(ok); out.write(s); out.flush(); out.close(); } }); loginFilter.setAuthenticationFailureHandler(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(); RespBean respBean = RespBean.error(exception.getMessage()); if (exception instanceof LockedException) { respBean.setMsg("账户被锁定,请联系管理员!"); } else if (exception instanceof CredentialsExpiredException) { respBean.setMsg("密码过期,请联系管理员!"); } else if (exception instanceof AccountExpiredException) { respBean.setMsg("账户过期,请联系管理员!"); } else if (exception instanceof DisabledException) { respBean.setMsg("账户被禁用,请联系管理员!"); } else if (exception instanceof BadCredentialsException) { respBean.setMsg("用户名或者密码输入错误,请重新输入!"); } out.write(new ObjectMapper().writeValueAsString(respBean)); out.flush(); out.close(); } }); loginFilter.setAuthenticationManager(authenticationManagerBean()); loginFilter.setFilterProcessesUrl("/doLogin"); return loginFilter; }
3.3 我们用自定义的 LoginFilter 实例代替 UsernamePasswordAuthenticationFilter
@Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() ... //省略 http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class); }

4 存储用户信息的原理

4.1 SecurityContextPersistenceFilter

UsernamePasswordAuthenticationFilter在这个过滤器之前,还有一个过滤器就是SecurityContextPersistenceFilter

public class SecurityContextPersistenceFilter extends GenericFilterBean { public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response); SecurityContext contextBeforeChainExecution = repo.loadContext(holder); try { SecurityContextHolder.setContext(contextBeforeChainExecution); chain.doFilter(holder.getRequest(), holder.getResponse()); } finally { SecurityContext contextAfterChainExecution = SecurityContextHolder .getContext(); SecurityContextHolder.clearContext(); repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse()); } } }

由于SecurityContextHolder.setContext(contextBeforeChainExecution);是通过ThreadLocal方式存储用户信息的,所以SecurityContextHolder.getContext()只能在当前线程获取用户信息,而SecurityContextPersistenceFilter的doFilter方法,SecurityContext contextBeforeChainExecution = repo.loadContext(holder);这段代码每次会从session中获取SecurityContext,并保存到SecurityContextHolder,这样每个请求都可以通过SecurityContextHolde获取用户信息。finally 中还有一步收尾操作,这一步很关键。这里从 SecurityContextHolder 中获取到 SecurityContext,获取到之后,会把 SecurityContextHolder 清空,然后调用 repo.saveContext 方法将获取到的 SecurityContext 存入 session 中

4.2 SecurityContextHolder.getContext()无法获取用户信息的原因

第一种可能是通过子线程获取

@GetMapping("/menu") public List<Menu> getMenusByHrId() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); System.out.println("主线程"+authentication) new Thread(new Runnable() { @Override public void run() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); System.out.println("子线程"+authentication); } }).start(); return menuService.getMenusByHrId(); }

第二种可能是该接口设置了不走security的过滤器链

@Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/css/**","/js/**","/index.html","/img/**","/fonts/**","/favicon.ico","/verifyCode"); }

5 jwt令牌

5.1 什么是有状态登录

​ 有状态服务,即服务端需要记录每次会话的客户端信息,从而识别客户端身份,根据用户身份进行请求的处理,典型的设计如Tomcat中的Session。例如登录:用户登录后,我们把用户的信息保存在服务端session中,并且给用户一个cookie值,记录对应的session,然后下次请求,用户携带cookie值来(这一步有浏览器自动完成),我们就能识别到对应session,从而找到用户的信息。这种方式目前来看最方便,但是也有一些缺陷,如下:1.服务端保存大量数据,增加服务端压力;2.服务端保存用户状态,不支持集群化部署。

5.2 什么是无状态登录

​ 服务的无状态性,即:1.服务端不保存任何客户端请求者信息;2.客户端的每次请求必须具备自描述信息,通过这些信息识别客户端身份。

5.3 jwt简介

​ JWT,全称是Json Web Token, 是一种JSON风格的轻量级的授权和身份认证规范,可实现无状态、分布式的Web应用授权。

5.4 jwt数据格式

JWT包含三部分数据:

  • Header:头部,通常头部有两部分信息:

    • 声明类型,这里是JWT
    • 加密算法,自定义

    我们会对头部进行Base64Url编码(可解码),得到第一部分数据。

  • Payload:载荷,就是有效数据,在官方文档中(RFC7519),这里给了7个示例信息:

    • iss (issuer):表示签发人
    • exp (expiration time):表示token过期时间
    • sub (subject):主题
    • aud (audience):受众
    • nbf (Not Before):生效时间
    • iat (Issued At):签发时间
    • jti (JWT ID):编号

    这部分也会采用Base64Url编码,得到第二部分数据。

  • Signature:签名,是整个数据的认证信息。一般根据前两步的数据,再加上服务的的密钥secret(密钥保存在服务端,不能泄露给客户端),通过Header中配置的加密算法生成。用于验证整个数据完整和可靠性。

6 jwt代码实战

6.1 环境搭建/依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency>
6.2 jwt过滤器配置

接下来提供两个和 JWT 相关的过滤器配置:

  1. 一个是用户登录的过滤器,在用户的登录的过滤器中校验用户是否登录成功,如果登录成功,则生成一个token返回给客户端,登录失败则给前端一个登录失败的提示。
  2. 第二个过滤器则是当其他请求发送来,校验token的过滤器,如果校验成功,就让请求继续执行。

这两个过滤器,我们分别来看,先看第一个:

String JWT = Jwts.builder() .setSubject(username) .setExpiration(new Date(System.currentTimeMillis() + EXPIRATIONTIME)) .claim("roleName", role) .claim("permissions", permissions) .signWith(SignatureAlgorithm.HS512, SECRET) .compact(); res.addHeader(HEADER_STRING, TOKEN_PREFIX + " " + JWT);

登录成功的回调里面,把用户名,权限,过期时间根据密钥加密成token

再来看第二个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"); 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); } }
「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论