在先後端分離的,後端微服務的狀況下,會話已經再也不適合保存在服務端,使用redis能夠保存會話,可是須要在redis集羣之間進行復制,若是用戶較多,保存的數據量也比較大。前端
JWT實現無狀態的會話機制,是一個解決方案。java
微服務集羣中的每一個服務,對外提供的都使用RESTful風格的接口。而RESTful風格的一個最重要的規範就是:服務的無狀態性,即:git
一、服務端不保存任何客戶端請求者信息
二、客戶端的每次請求必須具有自描述信息,經過這些信息識別客戶端身份github
一、首先客戶端發送帳戶名/密碼到服務端進行認證
二、認證經過後,服務端將用戶信息加密而且編碼成一個token,返回給客戶端
三、之後客戶端每次發送請求,都須要攜帶認證的token
四、服務端對客戶端發送來的token進行解密,判斷是否有效,而且獲取用戶登陸信息web
JWT 做爲一種規範,並無和某一種語言綁定在一塊兒,經常使用的Java 實現是GitHub 上的開源項目 jjwt,地址以下:https://github.com/jwtk/jjwtredis
說明:spring
一、一個登錄接口/login,用於獲取token
二、一個用戶接口/hello,用戶角色能夠訪問
三、一個管理接口/admin,管理角色能夠訪問數據庫
package com.baiziwan.authorize; import org.mybatis.spring.annotation.MapperScan; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.scheduling.annotation.EnableAsync; @SpringBootApplication @EnableDiscoveryClient @MapperScan({"com.baiziwan.authorize.*.dao", "com.baiziwan.authorize.*.*.dao"}) @EnableAsync public class AuthorizeApplication { private static final Logger LOGGER = LoggerFactory.getLogger(AuthorizeApplication.class); public static void main(String[] args) { try { SpringApplication.run(AuthorizeApplication.class, args); } catch (Exception e) { LOGGER.error("啓動失敗!", e); } } }
<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> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-oauth2-resource-server</artifactId> <version>5.1.3.RELEASE</version> </dependency>
package com.baiziwan.authorize.model; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.util.Collection; import java.util.List; public class User implements UserDetails { private String username; private String password; private List<GrantedAuthority> authorities; @Override public Collection<? extends GrantedAuthority> getAuthorities() { return authorities; } @Override public String getPassword() { return password; } 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; } }
package com.baiziwan.authorize.controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class HelloController { //須要jwt訪問,user角色能夠訪問 @GetMapping("/hello") public String hello() { return "hello jwt !"; } //須要jwt訪問,admin角色能夠訪問 @GetMapping("/admin") public String admin() { return "hello admin !"; } }
一、一個是用戶登陸的過濾器,在用戶的登陸的過濾器中校驗用戶是否登陸成功,若是登陸成功,則生成一個token返回給客戶端,登陸失敗則給前端一個登陸失敗的提示。
二、第二個過濾器則是當其餘請求發送來,校驗token的過濾器,若是校驗成功,就讓請求繼續執行。json
package com.baiziwan.authorize.filter; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.filter.GenericFilterBean; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import java.io.IOException; import java.util.List; 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); } }
package com.baiziwan.authorize.filter; import com.baiziwan.authorize.model.User; import com.fasterxml.jackson.databind.ObjectMapper; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; import java.util.Collection; import java.util.Date; 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(); } }
一、簡單起見,這裏並未鏈接數據庫,我直接在內存中配置了兩個用戶,兩個用戶具有不一樣的角色。
二、配置路徑規則時, /hello 接口必需要具有 user 角色才能訪問, /admin 接口必需要具有 admin 角色才能訪問,POST 請求而且是 /login 接口則能夠直接經過,其餘接口必須認證後才能訪問。後端
package com.baiziwan.authorize.config; import com.baiziwan.authorize.filter.JwtFilter; import com.baiziwan.authorize.filter.JwtLoginFilter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.password.NoOpPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @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(HttpMethod.GET,"/.well-known/jwks.json").permitAll() .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(); } }
一、獲取token
二、帶令牌訪問