1. 整體邏輯
1. SpringSecurity認(rèn)證的邏輯規(guī)則
啟動項目時,SpringBoot自動檢索所有帶@Configuration的注解,所以就將我們的WebSecurityConfig給加載了,這個config中,我們需要在configure(AuthenticationManagerBuilder auth)方法中注冊一個繼承自UserDetailsService的接口,這個接口中只有一個方法,那就是使用username獲取到數(shù)據(jù)庫中用戶信息并返回成UserDetail實體。這個方法需要我們按照我們的不同業(yè)務(wù)場景重寫
WebSecurityConfig
/**
* @description:
* @author: coderymy
* @create: 2020-10-01 13:54
* <p>
* 1\. 創(chuàng)建WebSecurityConfig 類繼承WebSecurityConfigurerAdapter
* 2\. 類上加上@EnableWebSecurity,注解中包括@Configuration注解
* <p>
* WebSecurityConfigurerAdapter聲明了一些默認(rèn)的安全特性
* (1)驗證所有的請求
* (2)可以使用springSecurity默認(rèn)的表單頁面進(jìn)行驗證登錄
* (3)允許用戶使用http請求進(jìn)行驗證
*/
/**
* 如何自定義認(rèn)證
* 1\. 實現(xiàn)并重寫configure(HttpSecurity http)方法,鑒權(quán),也就是判斷該用戶是否有訪問該api的權(quán)限
* <p>
* <p>
* 頁面顯示403錯誤,表示該用戶授權(quán)失?。?01代表該用戶認(rèn)證失?。┣岸丝梢允褂梅祷氐臓顟B(tài)碼來標(biāo)識如何給用戶展示
* 用2XX表示本次操作成功,用4XX表示是客戶端導(dǎo)致的失敗,用5XX表示是服務(wù)器引起的錯誤
*/
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
public static void main(String[] args) {
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String encode = passwordEncoder.encode("123");
System.out.println(encode);
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
AuthenticationManager manager = super.authenticationManagerBean();
return manager;
}
/**
* SpringSecurity5.X要求必須指定密碼加密方式,否則會在請求認(rèn)證的時候報錯
* 同樣的,如果指定了加密方式,就必須您的密碼在數(shù)據(jù)庫中存儲的是加密后的,才能比對成功
*
* @return
*/
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
//鑒權(quán)
@Override
protected void configure(HttpSecurity http) throws Exception {
/**
* 1\. HttpSecurity被聲明為鏈?zhǔn)秸{(diào)用
* 其中配置方法包括
* 1\. authorizeRequests()url攔截配置
* 2\. formLogin()表單驗證
* 3\. httpBasic()表單驗證
* 4\. csrf()提供的跨站請求偽造防護(hù)功能
*/
/**
* 2\. authorizeRequests目的是指定url進(jìn)行攔截的,也就是默認(rèn)這個url是“/”也就是所有的
* anyanyRequest()、antMatchers()和regexMatchers()三種方法來拼配系統(tǒng)的url。并指定安全策略
*/
http.authorizeRequests()
//這里指定什么樣的接口地址的請求,需要什么樣的權(quán)限 ANT模式的URL匹配器
.antMatchers("/select/**").hasRole("USER")//用戶可以有查詢權(quán)限
.antMatchers("/insert/**").hasRole("ADMIN")//管理員可以有插入權(quán)限權(quán)限
.antMatchers("/empower/**").hasRole("SUPERADMIN")//超級管理員才有賦權(quán)的權(quán)限
.antMatchers("/login/**").permitAll()//標(biāo)識list所有權(quán)限都可以直接訪問,即使不登錄也可以訪問。一般將login頁面放給這個權(quán)限
.and()
.formLogin()
// .loginProcessingUrl("/login/user")//用來定義什么樣的API請求時login請求
// .permitAll()//login請求需要是所有權(quán)限都可以的
.and().csrf().disable();
/**
* 將自定義的JWT過濾器加入configure中
*/
JWTAuthenticationFilter jwtAuthenticationFilter = new JWTAuthenticationFilter(this.authenticationManager());
http.addFilterBefore(jwtAuthenticationFilter, JWTAuthenticationFilter.class);
}
@Autowired
private MyUserDetailsService myUserDetailsService;
//認(rèn)證
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserDetailsService);
}
}
MyUserDetailsService
@Service
public class MyUserDetailsService implements UserDetailsService {
@Resource
private UsersRepository usersRepository;
/**
* 其實這樣就完成了認(rèn)證的過程,能獲取到數(shù)據(jù)庫中配置的用戶信息
*
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//獲取該用戶的信息
Users user = usersRepository.findByUsername(username);
if (user == null) {//用戶不存在報錯
throw new UsernameNotFoundException("用戶不存在");
}
/**
* 將roles信息轉(zhuǎn)換成SpringSecurity內(nèi)部的形式,即Authorities
* commaSeparatedStringToAuthorityList可以將使用,隔開的角色列表切割出來并賦值List
* 如果不行的話,也可以自己實現(xiàn)這個方法,只要拆分出來就可以了
*/
//注意,這里放入Authorities中的信息,都需要是以Role_開頭的,所以我們在數(shù)據(jù)庫中配置的都是這種格式的。當(dāng)我們使用hasRole做比對的時候,必須要是帶Role_開頭的。否則可以使用hasAuthority方法做比對
user.setAuthorities(AuthorityUtils.commaSeparatedStringToAuthorityList(user.getRoles()));
return user;
}
}
其實如果去掉上面的將自定義的JWT過濾器加入到過濾鏈中的話,這個認(rèn)證過程已經(jīng)完成了。使用下面的代碼就可以調(diào)用起整個認(rèn)證程序。
核心代碼
authenticate = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(userDto.getUsername(), userDto.getPassword()));
這一行就會將username和password放到認(rèn)證程序中進(jìn)行認(rèn)證。
也就是需要我們自己也邏輯讓他去觸發(fā)這個代碼的實現(xiàn)。就可以自動完成認(rèn)證程序了。就會觸發(fā)使用username獲取到數(shù)據(jù)庫用戶信息,然后經(jīng)過密碼加密比對之后會將認(rèn)證結(jié)果返回。
我們整合JWT其實也很簡單,其實就是將JWT的登錄部分的操作,使用過濾器封裝,將該過濾器放到整個認(rèn)證的過濾鏈中
2. 自定義JWT過濾器的配置
SpringSecurity過濾器的配置無非以下幾個條件
- 該過濾器過濾什么樣的API請求(也就是說什么樣的API請求會觸發(fā)該過濾器執(zhí)行)。配置被過濾的請求API
- 該過濾器做什么事
- 該過濾器執(zhí)行成功以及執(zhí)行失敗的各種情況該怎么做
- 該過濾器執(zhí)行的時機(jī)是什么樣的,也就是在過濾鏈之前還是之后執(zhí)行
先解決邏輯上以上三個問題的答案
- 我們需要攔截認(rèn)證請求,肯定是形如
xxx/login/xxx這種API接口的請求啦 - 這個過濾器會做什么事呢?
- 首先,我們需要進(jìn)行用戶名密碼的基礎(chǔ)驗證,也就是合不合法
- 我們需要調(diào)用起
SpringSecurity的默認(rèn)認(rèn)證程序 - 認(rèn)證程序執(zhí)行成功之后,我們需要按照用戶的信息以JWT的規(guī)則生成一個JWTToken并將其放入response中返回回去
- 認(rèn)證程序執(zhí)行失敗,也就是用戶登錄失敗,我們也需要將返回信息封裝起來返回給用戶
- 執(zhí)行成功,我們需要返回給用戶JWTToken信息。執(zhí)行失敗,我們也要友好提示用戶
- 如果排除其他的業(yè)務(wù)場景干擾,目前過濾鏈只有進(jìn)行鑒權(quán)時候才使用。所以針對不同的業(yè)務(wù)場景,這個過濾器放的地方其實是不一樣的。(之后我們的另一個JWTToken校驗的過濾器應(yīng)該需要在這個認(rèn)證的過濾器之后(兩個其實并不捕捉同樣的APi所以不會依次執(zhí)行。也不用太考慮這個問題))(我記得SpringCloud中的zuul網(wǎng)關(guān)的過濾器是可以自定義級別的。但是目前在SpringSecurity中尚未發(fā)現(xiàn)這種功能)
針對以上解答,下面用代碼來做展示(ps:序號依次對應(yīng)上面)
- 配置過濾器過濾地址
/**
* 下面是為了配置這個Manager
* 配置其攔截的API請求地址
*
* @param authenticationManager
*/
public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
super.setFilterProcessesUrl("/login/user");//這里指定什么樣的API請求會被這個過濾器攔截
}
- 配置過濾器職能
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
//這里是定義一個攔截器,在認(rèn)證方面的攔截器,當(dāng)請求的時候回攔截到這里面然后進(jìn)行身份認(rèn)證
//驗證用戶名密碼是否正確之后
Authentication authenticate = null;
try {
System.out.println("InputStream:" + request.getInputStream());
UserDto userDto = new ObjectMapper().readValue(request.getInputStream(), UserDto.class);//這個地方相當(dāng)于封裝一下請求,因為前臺請求的是user.username="xxx"這種對象的形式。
//對于這個過濾器攔截的接口,去調(diào)用SpringSecurity默認(rèn)的認(rèn)證程序,也就是去進(jìn)行SpringSecurity的認(rèn)證
authenticate = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(userDto.getUsername(), userDto.getPassword()));
return authenticate;
//如果返回成功,就進(jìn)入successfulAuthentication。返回失敗就進(jìn)入unsuccessfulAuthentication
//可以通過下面的定義來讓前端得到不同的返回從而向用戶展示不同的效果
//TODO 這個地方還有點問題,如果密碼錯誤了,就會報BadCredentialsException錯誤。需要看一下如果不讓這么報錯并讓他進(jìn)入到unsuccessfulAuthentication方法中
} catch (BadCredentialsException e) {//捕捉密碼驗證錯誤異常
log.info("密碼錯誤");
try {
this.unsuccessfulAuthentication(request, response, e);
} catch (IOException ex) {
ex.printStackTrace();
} catch (ServletException ex) {
ex.printStackTrace();
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
//當(dāng)身份驗證通過之后,會進(jìn)入這里,這里可以定義成功的返回
Users user = (Users) authResult.getPrincipal();//將principal中的信息轉(zhuǎn)換成User對象
JwtUtil util = new JwtUtil(secretKey, SignatureAlgorithm.HS256);
Map<String, Object> map = new HashMap<>();
map.put("username", user.getUsername());
map.put("password", user.getPassword());
String jwtToken = util.encode("tom", 30000, map);
response.addHeader("Authorizations", jwtToken);
user.setJwtToken(jwtToken);
ResponseUtil.write(response, JSONObject.toJSONString(ResultUtil.success(user)));
System.out.println(response.getHeaderNames());
// super.successfulAuthentication(request, response, chain, authResult);
//注意,不要使用默認(rèn)的super來定義,否則上述會失效的
}
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
//TODO 得看一下為啥會報這個錯誤
System.out.println("認(rèn)證失敗");
ResponseUtil.write(response, JSONObject.toJSONString(ResultUtil.error("登錄失敗,賬號密碼錯誤")));
}
- 執(zhí)行失敗與成功,分別是2中的
unsuccessfulAuthentication和successfulAuthentication方法 - 配置過濾鏈執(zhí)行的位置
在WebSecurityConfig中的configure(HttpSecurity http)方法中
JWTAuthenticationFilter jwtAuthenticationFilter = new JWTAuthenticationFilter(this.authenticationManager());
http.addFilterBefore(jwtAuthenticationFilter, JWTAuthenticationFilter.class);
完成了以上的配置,前臺就可以使用/login/user來進(jìn)行登錄操作了。登錄成功會返回一個JSON對象來供前端判斷成功與否
2. 代碼結(jié)果
全部代碼奉上,隨意寫的注釋有點多,不看的可以給刪掉
- WebSecurityConfig
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationManager;
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.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* @description:
* @author: coderymy
* @create: 2020-10-01 13:54
* <p>
* 1\. 創(chuàng)建WebSecurityConfig 類繼承WebSecurityConfigurerAdapter
* 2\. 類上加上@EnableWebSecurity,注解中包括@Configuration注解
* <p>
* WebSecurityConfigurerAdapter聲明了一些默認(rèn)的安全特性
* (1)驗證所有的請求
* (2)可以使用springSecurity默認(rèn)的表單頁面進(jìn)行驗證登錄
* (3)允許用戶使用http請求進(jìn)行驗證
*/
/**
* 如何自定義認(rèn)證
* 1\. 實現(xiàn)并重寫configure(HttpSecurity http)方法,鑒權(quán),也就是判斷該用戶是否有訪問該api的權(quán)限
* <p>
* <p>
* 頁面顯示403錯誤,表示該用戶授權(quán)失?。?01代表該用戶認(rèn)證失敗)前端可以使用返回的狀態(tài)碼來標(biāo)識如何給用戶展示
* 用2XX表示本次操作成功,用4XX表示是客戶端導(dǎo)致的失敗,用5XX表示是服務(wù)器引起的錯誤
*/
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
public static void main(String[] args) {
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String encode = passwordEncoder.encode("123");
System.out.println(encode);
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
AuthenticationManager manager = super.authenticationManagerBean();
return manager;
}
/**
* SpringSecurity5.X要求必須指定密碼加密方式,否則會在請求認(rèn)證的時候報錯
* 同樣的,如果指定了加密方式,就必須您的密碼在數(shù)據(jù)庫中存儲的是加密后的,才能比對成功
*
* @return
*/
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
//鑒權(quán)
@Override
protected void configure(HttpSecurity http) throws Exception {
/**
* 1\. HttpSecurity被聲明為鏈?zhǔn)秸{(diào)用
* 其中配置方法包括
* 1\. authorizeRequests()url攔截配置
* 2\. formLogin()表單驗證
* 3\. httpBasic()表單驗證
* 4\. csrf()提供的跨站請求偽造防護(hù)功能
*/
/**
* 2\. authorizeRequests目的是指定url進(jìn)行攔截的,也就是默認(rèn)這個url是“/”也就是所有的
* anyanyRequest()、antMatchers()和regexMatchers()三種方法來拼配系統(tǒng)的url。并指定安全策略
*/
http.authorizeRequests()
//這里指定什么樣的接口地址的請求,需要什么樣的權(quán)限 ANT模式的URL匹配器
.antMatchers("/select/**").hasRole("USER")//用戶可以有查詢權(quán)限
.antMatchers("/insert/**").hasRole("ADMIN")//管理員可以有插入權(quán)限權(quán)限
.antMatchers("/empower/**").hasRole("SUPERADMIN")//超級管理員才有賦權(quán)的權(quán)限
.antMatchers("/login/**").permitAll()//標(biāo)識list所有權(quán)限都可以直接訪問,即使不登錄也可以訪問。一般將login頁面放給這個權(quán)限
.and()
.formLogin()
// .loginProcessingUrl("/login/user")//用來定義什么樣的API請求時login請求
// .permitAll()//login請求需要是所有權(quán)限都可以的
.and().csrf().disable();
/**
* 將自定義過濾器加入configure中
*/
JWTAuthenticationFilter jwtAuthenticationFilter = new JWTAuthenticationFilter(this.authenticationManager());
http.addFilterBefore(jwtAuthenticationFilter, JWTAuthenticationFilter.class);
}
@Autowired
private MyUserDetailsService myUserDetailsService;
//認(rèn)證
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserDetailsService);
}
}
- JWTAuthenticationFilter
import com.alibaba.fastjson.JSONObject;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.huanong.avatarma.basic.entity.Users;
import com.huanong.avatarma.basic.model.vo.UserDto;
import com.huanong.avatarma.common.util.JwtUtil;
import com.huanong.avatarma.common.util.ResponseUtil;
import com.huanong.avatarma.common.util.ResultUtil;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.stereotype.Component;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* @description: jwt攔截器
* 這個定義完成之后,想要生效還需要將其加入到過濾鏈中
* @author: coderymy
* @create: 2020-10-02 09:36
**/
@Slf4j
public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
/**
* 驗證用戶名密碼是否正確之后,生成一個token,并將token返回客戶端
*
* @param request
* @param response
* @return
* @throws AuthenticationException
*/
private String secretKey = "ILoveDanChaoFan";
private AuthenticationManager authenticationManager;
/**
* 下面是為了配置這個Manager
* 配置其攔截的API請求地址
*
* @param authenticationManager
*/
public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
super.setFilterProcessesUrl("/login/user");//這里指定什么樣的API請求會被這個過濾器攔截
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
//這里是定義一個攔截器,在認(rèn)證方面的攔截器,當(dāng)請求的時候回攔截到這里面然后進(jìn)行身份認(rèn)證
//驗證用戶名密碼是否正確之后
Authentication authenticate = null;
try {
System.out.println("InputStream:" + request.getInputStream());
UserDto userDto = new ObjectMapper().readValue(request.getInputStream(), UserDto.class);//這個地方相當(dāng)于封裝一下請求,因為前臺請求的是user.username="xxx"這種對象的形式。
//對于這個過濾器攔截的接口,去調(diào)用SpringSecurity默認(rèn)的認(rèn)證程序,也就是去進(jìn)行SpringSecurity的認(rèn)證
authenticate = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(userDto.getUsername(), userDto.getPassword()));
return authenticate;
//如果返回成功,就進(jìn)入successfulAuthentication。返回失敗就進(jìn)入unsuccessfulAuthentication
//可以通過下面的定義來讓前端得到不同的返回從而向用戶展示不同的效果
//TODO 這個地方還有點問題,如果密碼錯誤了,就會報BadCredentialsException錯誤。需要看一下如果不讓這么報錯并讓他進(jìn)入到unsuccessfulAuthentication方法中
} catch (BadCredentialsException e) {//捕捉密碼驗證錯誤異常
log.info("密碼錯誤");
try {
this.unsuccessfulAuthentication(request, response, e);
} catch (IOException ex) {
ex.printStackTrace();
} catch (ServletException ex) {
ex.printStackTrace();
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
//當(dāng)身份驗證通過之后,會進(jìn)入這里,這里可以定義成功的返回
Users user = (Users) authResult.getPrincipal();//將principal中的信息轉(zhuǎn)換成User對象
JwtUtil util = new JwtUtil(secretKey, SignatureAlgorithm.HS256);
Map<String, Object> map = new HashMap<>();
map.put("username", user.getUsername());
map.put("password", user.getPassword());
String jwtToken = util.encode("tom", 30000, map);
response.addHeader("Authorizations", jwtToken);
user.setJwtToken(jwtToken);
ResponseUtil.write(response, JSONObject.toJSONString(ResultUtil.success(user)));
System.out.println(response.getHeaderNames());
// super.successfulAuthentication(request, response, chain, authResult);
//注意,不要使用默認(rèn)的super來定義,否則上述會失效的
}
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
//TODO 得看一下為啥會報這個錯誤
System.out.println("認(rèn)證失敗");
ResponseUtil.write(response, JSONObject.toJSONString(ResultUtil.error("登錄失敗,賬號密碼錯誤")));
}
}
- MyUserDetailsService
import com.huanong.avatarma.basic.dao.UsersRepository;
import com.huanong.avatarma.basic.entity.Users;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
/**
* @description:
* @author: coderymy
* @create: 2020-10-01 15:55
**/
@Service
public class MyUserDetailsService implements UserDetailsService {
@Resource
private UsersRepository usersRepository;
/**
* 其實這樣就完成了認(rèn)證的過程,能獲取到數(shù)據(jù)庫中配置的用戶信息
*
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//獲取該用戶的信息
Users user = usersRepository.findByUsername(username);
if (user == null) {//用戶不存在報錯
throw new UsernameNotFoundException("用戶不存在");
}
/**
* 將roles信息轉(zhuǎn)換成SpringSecurity內(nèi)部的形式,即Authorities
* commaSeparatedStringToAuthorityList可以將使用,隔開的角色列表切割出來并賦值List
* 如果不行的話,也可以自己實現(xiàn)這個方法,只要拆分出來就可以了
*/
//注意,這里放入Authorities中的信息,都需要是以Role_開頭的,所以我們在數(shù)據(jù)庫中配置的都是這種格式的。當(dāng)我們使用hasRole做比對的時候,必須要是帶Role_開頭的。否則可以使用hasAuthority方法做比對
user.setAuthorities(AuthorityUtils.commaSeparatedStringToAuthorityList(user.getRoles()));
return user;
}
}
- Users
@Data
@ToString
@Entity
@Table(name = "users")
public class Users implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String password;
private boolean enable;
private String roles;
private Date createDate;
private Date modifyDate;
@Transient//這個注解可以幫助在entity中添加表中沒有的字段
private List<GrantedAuthority> authorities;
@Transient
private String jwtToken;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
//這個本身是對應(yīng)roles字段的,但是因為結(jié)構(gòu)不一致,所以重新創(chuàng)建一個,后續(xù)補(bǔ)充這部分
return this.authorities;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
public void setAuthorities(List<GrantedAuthority> authorities) {
this.authorities = authorities;
}
@Override
public boolean isEnabled() {
return this.enable;
}
}
- Result
import lombok.Data;
import lombok.ToString;
/**
* @description:
* @author: coderymy
* @create: 2020-10-02 13:24
**/
@Data
@ToString
public class Result {
private Integer code;
private String msg;
private Object data;
}
- JwtUtil
/**
* @description:
* @author: coderymy
* @create: 2020-10-02 09:24
**/
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.apache.commons.codec.binary.Base64;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.*;
/*
* 總的來說,工具類中有三個方法
* 獲取JwtToken,獲取JwtToken中封裝的信息,判斷JwtToken是否存在
* 1\. encode(),參數(shù)是=簽發(fā)人,存在時間,一些其他的信息=。返回值是JwtToken對應(yīng)的字符串
* 2\. decode(),參數(shù)是=JwtToken=。返回值是荷載部分的鍵值對
* 3\. isVerify(),參數(shù)是=JwtToken=。返回值是這個JwtToken是否存在
* */
public class JwtUtil {
//創(chuàng)建默認(rèn)的秘鑰和算法,供無參的構(gòu)造方法使用
private static final String defaultbase64EncodedSecretKey = "badbabe";
private static final SignatureAlgorithm defaultsignatureAlgorithm = SignatureAlgorithm.HS256;
public JwtUtil() {
this(defaultbase64EncodedSecretKey, defaultsignatureAlgorithm);
}
private final String base64EncodedSecretKey;
private final SignatureAlgorithm signatureAlgorithm;
public JwtUtil(String secretKey, SignatureAlgorithm signatureAlgorithm) {
this.base64EncodedSecretKey = Base64.encodeBase64String(secretKey.getBytes());
this.signatureAlgorithm = signatureAlgorithm;
}
/*
*這里就是產(chǎn)生jwt字符串的地方
* jwt字符串包括三個部分
* 1\. header
* -當(dāng)前字符串的類型,一般都是“JWT”
* -哪種算法加密,“HS256”或者其他的加密算法
* 所以一般都是固定的,沒有什么變化
* 2\. payload
* 一般有四個最常見的標(biāo)準(zhǔn)字段(下面有)
* iat:簽發(fā)時間,也就是這個jwt什么時候生成的
* jti:JWT的唯一標(biāo)識
* iss:簽發(fā)人,一般都是username或者userId
* exp:過期時間
*
* */
public String encode(String iss, long ttlMillis, Map<String, Object> claims) {
//iss簽發(fā)人,ttlMillis生存時間,claims是指還想要在jwt中存儲的一些非隱私信息
if (claims == null) {
claims = new HashMap<>();
}
long nowMillis = System.currentTimeMillis();
JwtBuilder builder = Jwts.builder()
.setClaims(claims)
.setId(UUID.randomUUID().toString())//2\. 這個是JWT的唯一標(biāo)識,一般設(shè)置成唯一的,這個方法可以生成唯一標(biāo)識
.setIssuedAt(new Date(nowMillis))//1\. 這個地方就是以毫秒為單位,換算當(dāng)前系統(tǒng)時間生成的iat
.setSubject(iss)//3\. 簽發(fā)人,也就是JWT是給誰的(邏輯上一般都是username或者userId)
.signWith(signatureAlgorithm, base64EncodedSecretKey);//這個地方是生成jwt使用的算法和秘鑰
if (ttlMillis >= 0) {
long expMillis = nowMillis + ttlMillis;
Date exp = new Date(expMillis);//4\. 過期時間,這個也是使用毫秒生成的,使用當(dāng)前時間+前面?zhèn)魅氲某掷m(xù)時間生成
builder.setExpiration(exp);
}
return builder.compact();
}
//相當(dāng)于encode的方向,傳入jwtToken生成對應(yīng)的username和password等字段。Claim就是一個map
//也就是拿到荷載部分所有的鍵值對
public Claims decode(String jwtToken) {
// 得到 DefaultJwtParser
return Jwts.parser()
// 設(shè)置簽名的秘鑰
.setSigningKey(base64EncodedSecretKey)
// 設(shè)置需要解析的 jwt
.parseClaimsJws(jwtToken)
.getBody();
}
//判斷jwtToken是否合法
public boolean isVerify(String jwtToken) {
//這個是官方的校驗規(guī)則,這里只寫了一個”校驗算法“,可以自己加
Algorithm algorithm = null;
switch (signatureAlgorithm) {
case HS256:
algorithm = Algorithm.HMAC256(Base64.decodeBase64(base64EncodedSecretKey));
break;
default:
throw new RuntimeException("不支持該算法");
}
JWTVerifier verifier = JWT.require(algorithm).build();
verifier.verify(jwtToken); // 校驗不通過會拋出異常
//判斷合法的標(biāo)準(zhǔn):1\. 頭部和荷載部分沒有篡改過。2\. 沒有過期
return true;
}
public static void main(String[] args) {
JwtUtil util = new JwtUtil("tom", SignatureAlgorithm.HS256);
//以tom作為秘鑰,以HS256加密
Map<String, Object> map = new HashMap<>();
map.put("username", "tom");
map.put("password", "123456");
map.put("age", 20);
String jwtToken = util.encode("tom", 30000, map);
System.out.println(jwtToken);
util.decode(jwtToken).entrySet().forEach((entry) -> {
System.out.println(entry.getKey() + ": " + entry.getValue());
});
}
}
- ResponseUtil、ResultUtil、repository等不做展示。需要的可以聯(lián)系我,這個是基礎(chǔ)性的東西
- 依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.11.0</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>1.0.10.RELEASE</version>
</dependency>