在先后端分离的项目中,登陆策略也有很多,不过 JWT 算是目前比较流行的一种解决方案了,本文就和你们来分享一下如何将 Spring Security 和 JWT 结合在一块儿使用,进而实现先后端分离时的登陆解决方案。前端
有状态服务,即服务端须要记录每次会话的客户端信息,从而识别客户端身份,根据用户身份进行请求的处理,典型的设计如Tomcat中的Session。例如登陆:用户登陆后,咱们把用户的信息保存在服务端session中,而且给用户一个cookie值,记录对应的session,而后下次请求,用户携带cookie值来(这一步有浏览器自动完成),咱们就能识别到对应session,从而找到用户的信息。这种方式目前来看最方便,可是也有一些缺陷,以下:java
微服务集群中的每一个服务,对外提供的都使用RESTful风格的接口。而RESTful风格的一个最重要的规范就是:服务的无状态性,即:git
那么这种无状态性有哪些好处呢?github
无状态登陆的流程:web
JWT,全称是Json Web Token, 是一种JSON风格的轻量级的受权和身份认证规范,可实现无状态、分布式的Web应用受权: redis
JWT 做为一种规范,并无和某一种语言绑定在一块儿,经常使用的Java 实现是GitHub 上的开源项目 jjwt,地址以下:https://github.com/jwtk/jjwt
算法
JWT包含三部分数据:spring
Header:头部,一般头部有两部分信息:数据库
咱们会对头部进行Base64Url编码(可解码),获得第一部分数据。json
Payload:载荷,就是有效数据,在官方文档中(RFC7519),这里给了7个示例信息:
这部分也会采用Base64Url编码,获得第二部分数据。
生成的数据格式以下图:
注意,这里的数据经过 .
隔开成了三部分,分别对应前面提到的三部分,另外,这里数据是不换行的,图片换行只是为了展现方便而已。
流程图:
步骤翻译:
由于JWT签发的token中已经包含了用户的身份信息,而且每次请求都会携带,这样服务的就无需保存用户信息,甚至无需去数据库查询,这样就彻底符合了RESTful的无状态规范。
说了这么多,JWT 也不是完美无缺,由客户端维护登陆状态带来的一些问题在这里依然存在,举例以下:
说了这么久,接下来咱们就来看看这个东西到底要怎么用?
首先咱们来建立一个Spring Boot项目,建立时须要添加Spring Security依赖,建立完成后,添加 jjwt
依赖,完整的pom.xml文件以下:
<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>
而后在项目中建立一个简单的 User 对象实现 UserDetails 接口,以下:
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; } //省略getter/setter }
这个就是咱们的用户对象,先放着备用,再建立一个HelloController,内容以下:
@RestController public class HelloController { @GetMapping("/hello") public String hello() { return "hello jwt !"; } @GetMapping("/admin") public String admin() { return "hello admin !"; } }
HelloController 很简单,这里有两个接口,设计是 /hello
接口能够被具备 user 角色的用户访问,而 /admin
接口则能够被具备 admin 角色的用户访问。
接下来提供两个和 JWT 相关的过滤器配置:
这两个过滤器,咱们分别来看,先看第一个:
public class JwtLoginFilter extends AbstractAuthenticationProcessingFilter { protected 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(); } }
关于这个类,我说以下几点:
,
链接起来,而后再利用Jwts去生成token,按照代码的顺序,生成过程一共配置了四个参数,分别是用户角色、主题、过时时间以及加密算法和密钥,而后将生成的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); } }
关于这个过滤器,我说以下几点:
如此以后,两个和JWT相关的过滤器就算配置好了。
接下来咱们来配置 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("sang") .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() .addFilterBefore(new JwtLoginFilter("/login",authenticationManager()),UsernamePasswordAuthenticationFilter.class) .addFilterBefore(new JwtFilter(),UsernamePasswordAuthenticationFilter.class) .csrf().disable(); } }
/hello
接口必需要具有 user 角色才能访问, /admin
接口必需要具有 admin 角色才能访问,POST 请求而且是 /login
接口则能够直接经过,其余接口必须认证后才能访问。作完这些以后,咱们的环境就算彻底搭建起来了,接下来启动项目而后在 POSTMAN 中进行测试,以下:
登陆成功后返回的字符串就是通过 base64url 转码的token,一共有三部分,经过一个 .
隔开,咱们能够对第一个 .
以前的字符串进行解码,即Header,以下:
再对两个 .
之间的字符解码,即 payload:
能够看到,咱们设置信息,因为base64并非加密方案,只是一种编码方案,所以,不建议将敏感的用户信息放到token中。
接下来再去访问 /hello
接口,注意认证方式选择 Bearer Token,Token值为刚刚获取到的值,以下:
能够看到,访问成功。
这就是 JWT 结合 Spring Security 的一个简单用法,讲真,若是实例容许,相似的需求我仍是推荐使用 OAuth2 中的 password 模式。
不知道大伙有没有看懂呢?若是没看懂,松哥还有一个关于这个知识点的视频教程,以下:
如何获取这个视频教程呢?很简单,将本文转发到一个超过100人的微信群中(QQ群不算,松哥是群主的微信群也不算,群要为Java方向),或者多个微信群中,只要累计人数达到100人便可,而后加松哥微信,截图发给松哥便可获取资料。
关注公众号【江南一点雨】,专一于 Spring Boot+微服务以及先后端分离等全栈技术,按期视频教程分享,关注后回复 Java ,领取松哥为你精心准备的 Java 干货!