在先后端分离的项目中,登陆策略也有很多,不过 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编码,获得第二部分数据。
Signature:签名,是整个数据的认证信息。通常根据前两步的数据,再加上服务的的密钥secret(密钥保存在服务端,不能泄露给客户端),经过Header中配置的加密算法生成。用于验证整个数据完整和可靠性。
生成的数据格式以下图:
注意,这里的数据经过 .
隔开成了三部分,分别对应前面提到的三部分,另外,这里数据是不换行的,图片换行只是为了展现方便而已。
流程图:
步骤翻译:
由于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 干货!