Spring-Security & JWT 實現(xiàn) token
一、JWT
//www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html二、項目環(huán)境搭建
2.1 引入依賴
pom.xml
<dependencies><dependency><groupId>javax.xml.bind</groupId><artifactId>jaxb-api</artifactId><version>2.3.0</version></dependency><dependency><groupId>com.sun.xml.bind</groupId><artifactId>jaxb-impl</artifactId><version>2.3.0</version></dependency><dependency><groupId>com.sun.xml.bind</groupId><artifactId>jaxb-core</artifactId><version>2.3.0</version></dependency><dependency><groupId>javax.activation</groupId><artifactId>activation</artifactId><version>1.1.1</version></dependency><!-- spring security --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><!-- jwt --><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.1</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- mybatisplus與springboot整合 --><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.2.0</version></dependency><!-- mybatis plus 代碼生成器依賴 --><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-generator</artifactId><version>3.2.0</version></dependency><!--swagger--><dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger2</artifactId><version>2.9.2</version></dependency><dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger-ui</artifactId><version>2.9.2</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><!-- mysql驅(qū)動 --><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope><exclusions><exclusion><groupId>org.junit.vintage</groupId><artifactId>junit-vintage-engine</artifactId></exclusion></exclusions></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>
主要是要引入Spring Secruity和jwt的依賴。
2.2 實體類(User)
User.java
public class User implements Serializable {(value = "user_id",type= IdType.AUTO)private int userId;private String userName;private String Password;private String userAge;private String Role;public int getUserId() {return userId;}public void setUserId(int userId) {this.userId = userId;}public String getUserName() {return userName;}public void setUserName(String userName) {this.userName = userName;}public String getPassword() {return Password;}public void setPassword(String password) {Password = password;}public String getUserAge() {return userAge;}public void setUserAge(String userAge) {this.userAge = userAge;}public String getRole() {return Role;}public void setRole(String role) {Role = role;}}
2.3 jwt工具類
JwtTokenUtils.java
public class JwtTokenUtils {public static final String TOKEN_HEADER = "token";public static final String TOKEN_PREFIX = "";private static final String SECRET = "jwtsecretdemo";private static final String ISS = "echisan";// 過期時間是3600秒,即是1個小時private static final long EXPIRATION = 3600L;// 選擇了記住我之后的過期時間為7天private static final long EXPIRATION_REMEMBER = 604800L;// 創(chuàng)建tokenpublic static String createToken(String username, boolean isRememberMe) {long expiration = isRememberMe ? EXPIRATION_REMEMBER : EXPIRATION;return Jwts.builder().signWith(SignatureAlgorithm.HS512, SECRET).setIssuer(ISS).setSubject(username).setIssuedAt(new Date()).setExpiration(new Date(System.currentTimeMillis() + expiration * 1000)).compact();}// 從token中獲取用戶名public static String getUsername(String token){return getTokenBody(token).getSubject();}// 是否已過期public static boolean isExpiration(String token){return getTokenBody(token).getExpiration().before(new Date());}private static Claims getTokenBody(String token){return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody();}}
jwt工具類,對jjwt封裝一下方便調(diào)用。
2.4 Dao層
因為使用的是mybatis plus,所以沒有使用mapper.xml書寫sql語句,直接調(diào)用提供的CRUD。
UserDao.java
public interface UserDao extends BaseMapper<User> {}
2.5 ServiceImpl層
UserDetailsServiceImpl.java
public class UserDetailsServiceImpl implements UserDetailsService {private UserDao userDao;public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {Map<String, Object> map = new HashMap<>();map.put("user_name", s);User user = userDao.selectByMap(map).get(0);return new JwtUser(user);}}
注意:這個serviceImpl實現(xiàn)的接口UserDetailsService是框架提供的。
使用springSecurity需要實現(xiàn)
UserDetailsService接口供權(quán)限框架調(diào)用,該方法只需要實現(xiàn)一個方法就可以了,那就是根據(jù)用戶名去獲取用戶,這里使用的是mybatis plus提供的操作接口。
接著去實現(xiàn)一下剛才返回的UserDetails
public class JwtUser implements UserDetails {private Integer id;private String username;private String password;private Collection<? extends GrantedAuthority> authorities;public JwtUser() {}// 寫一個能直接使用user創(chuàng)建jwtUser的構(gòu)造器public JwtUser(User user) {id = user.getUserId();username = user.getUserName();password = user.getPassword();authorities = Collections.singleton(new SimpleGrantedAuthority(user.getRole()));}// 獲取權(quán)限信息,目前博主只會拿來存角色。。public Collection<? extends GrantedAuthority> getAuthorities() {return authorities;}public String getPassword() {return password;}public String getUsername() {return username;}// 賬號是否未過期,默認(rèn)是false,記得要改一下public boolean isAccountNonExpired() {return true;}// 賬號是否未鎖定,默認(rèn)是false,記得也要改一下public boolean isAccountNonLocked() {return true;}// 賬號憑證是否未過期,默認(rèn)是false,記得還要改一下public boolean isCredentialsNonExpired() {return true;}// 這個有點(diǎn)抽象不會翻譯,默認(rèn)也是false,記得改一下public boolean isEnabled() {return true;}// 我自己重寫打印下信息看的public String toString() {return "JwtUser{" +"id=" + id +", username='" + username + '\'' +", password='" + password + '\'' +", authorities=" + authorities +'}';}}
三、配置攔截器
這邊需要實現(xiàn)兩個過濾器。使用JWTAuthenticationFilter去進(jìn)行用戶賬號的驗證,使用JWTAuthorizationFilter去進(jìn)行用戶權(quán)限的驗證。
3.1 JWTAuthenticationFilter
JWTAuthenticationFilter繼承于UsernamePasswordAuthenticationFilter
該攔截器用于獲取用戶登錄的信息,只需創(chuàng)建一個token并調(diào)用authenticationManager.authenticate()讓spring-security去進(jìn)行驗證就可以了,不用自己查數(shù)據(jù)庫再對比密碼了,這一步交給spring去操作。
這個操作有點(diǎn)像是shiro的subject.login(new UsernamePasswordToken()),驗證的事情交給框架。
JWTAuthenticationFilter.java
public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {private AuthenticationManager authenticationManager;public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {this.authenticationManager = authenticationManager;}public Authentication attemptAuthentication(HttpServletRequest request,HttpServletResponse response) throws AuthenticationException {// 從輸入流中獲取到登錄的信息try {LoginUser loginUser = new ObjectMapper().readValue(request.getInputStream(), LoginUser.class);return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(loginUser.getUsername(), loginUser.getPassword(), new ArrayList<>()));} catch (IOException e) {e.printStackTrace();return null;}}// 成功驗證后調(diào)用的方法// 如果驗證成功,就生成token并返回protected void successfulAuthentication(HttpServletRequest request,HttpServletResponse response,FilterChain chain,Authentication authResult) throws IOException, ServletException {// 查看源代碼會發(fā)現(xiàn)調(diào)用getPrincipal()方法會返回一個實現(xiàn)了`UserDetails`接口的對象// 所以就是JwtUser啦JwtUser jwtUser = (JwtUser) authResult.getPrincipal();System.out.println("jwtUser:" + jwtUser.toString());String token = JwtTokenUtils.createToken(jwtUser.getUsername(), false);// 返回創(chuàng)建成功的token// 但是這里創(chuàng)建的token只是單純的token// 按照jwt的規(guī)定,最后請求的格式應(yīng)該是 `Bearer token`response.setHeader("token", JwtTokenUtils.TOKEN_PREFIX + token);}// 這是驗證失敗時候調(diào)用的方法protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {response.getWriter().write("authentication failed, reason: " + failed.getMessage());}}
這里還用到了LoginUser這個實體類,也是需要自己定義一下的。
LoginUser.java
public class LoginUser {private String username;private String password;private Integer rememberMe;public String getUsername() {return username;}public void setUsername(String username) {this.username = username;}public String getPassword() {return password;}public void setPassword(String password) {this.password = password;}public Integer getRememberMe() {return rememberMe;}public void setRememberMe(Integer rememberMe) {this.rememberMe = rememberMe;}}
3.2 JWTAuthorizationFilter
驗證成功當(dāng)然就是進(jìn)行鑒權(quán)了,每一次需要權(quán)限的請求都需要檢查該用戶是否有該權(quán)限去操作該資源,當(dāng)然這也是框架幫我們做的,那么我們需要做什么呢?很簡單,只要告訴spring-security該用戶是否已登錄,是什么角色,擁有什么權(quán)限就可以了。
JWTAuthenticationFilter繼承于BasicAuthenticationFilter,至于為什么要繼承這個我也不太清楚了,這個我也是網(wǎng)上看到的其中一種實現(xiàn),實在springSecurity苦手,不過我覺得不繼承這個也沒事呢(實現(xiàn)以下filter接口或者繼承其他filter實現(xiàn)子類也可以吧)只要確保過濾器的順序,JWTAuthorizationFilter在JWTAuthenticationFilter后面就沒問題了。
JWTAuthorizationFilter.java
public class JWTAuthorizationFilter extends BasicAuthenticationFilter {public JWTAuthorizationFilter(AuthenticationManager authenticationManager) {super(authenticationManager);}protected void doFilterInternal(HttpServletRequest request,HttpServletResponse response,FilterChain chain) throws IOException, ServletException {String tokenHeader = request.getHeader(JwtTokenUtils.TOKEN_HEADER);// 如果請求頭中沒有Authorization信息則直接放行了if (tokenHeader == null || !tokenHeader.startsWith(JwtTokenUtils.TOKEN_PREFIX)) {chain.doFilter(request, response);return;}// 如果請求頭中有token,則進(jìn)行解析,并且設(shè)置認(rèn)證信息SecurityContextHolder.getContext().setAuthentication(getAuthentication(tokenHeader));super.doFilterInternal(request, response, chain);}// 這里從token中獲取用戶信息并新建一個tokenprivate UsernamePasswordAuthenticationToken getAuthentication(String tokenHeader) {String token = tokenHeader.replace(JwtTokenUtils.TOKEN_PREFIX, "");String username = JwtTokenUtils.getUsername(token);if (username != null){return new UsernamePasswordAuthenticationToken(username, null, new ArrayList<>());}return null;}}
3.3 配置SpringSecurity
需要開啟一下注解@EnableWebSecurity然后再繼承一下WebSecurityConfigurerAdapter就可以啦
WebSecurityConfig.java
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {// 因為UserDetailsService的實現(xiàn)類實在太多啦,這里設(shè)置一下我們要注入的實現(xiàn)類("userDetailsServiceImpl")private UserDetailsService userDetailsService;// 加密密碼的,安全第一嘛~public BCryptPasswordEncoder bCryptPasswordEncoder(){return new BCryptPasswordEncoder();}protected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());}protected void configure(HttpSecurity http) throws Exception {http.cors().and().csrf().disable().authorizeRequests()// 測試用資源,需要驗證了的用戶才能訪問.antMatchers("/tasks/**").authenticated()// 其他都放行了.anyRequest().permitAll().and().addFilter(new JWTAuthenticationFilter(authenticationManager())).addFilter(new JWTAuthorizationFilter(authenticationManager()))// 不需要session.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);}CorsConfigurationSource corsConfigurationSource() {final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());return source;}}
四、測試
4.1 注冊
AuthController.java
public class AuthController {// 為了減少篇幅就不寫service接口了private UserDao userDao;private BCryptPasswordEncoder bCryptPasswordEncoder;public String registerUser( Map<String,String> registerUser){User user = new User();user.setUserName(registerUser.get("username"));// 記得注冊的時候把密碼加密一下user.setPassword(bCryptPasswordEncoder.encode(registerUser.get("password")));user.setRole("ROLE_USER");int result = userDao.insert(user);return Integer.toString(result);}}
4.2 登陸
根據(jù)UsernamePasswordAuthenticationFilter的源代碼,可以看出登錄默認(rèn)是/login
public UsernamePasswordAuthenticationFilter() {super(new AntPathRequestMatcher("/login", "POST"));}
當(dāng)然也可以自定義,只需要在JWTAuthenticationFilter的構(gòu)造方法中加入下面那一句話就可以啦
public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {this.authenticationManager = authenticationManager;super.setFilterProcessesUrl("/auth/login");}
4.3 接口驗證
helloController.java
public class HelloController {public String hello() {return "hello jwt !";}public String admin() {return "hello admin !";}}
4.4 測試結(jié)果
先是注冊

登陸
這是可以獲取 token

接口訪問測試

需要將 token 加上才可以訪問成功。
記得點(diǎn)「贊」和「在看」↓
愛你們
