1. 整體邏輯
1. SpringSecurity認(rèn)證的邏輯規(guī)則
啟動(dòng)項(xiàng)目時(shí),SpringBoot自動(dòng)檢索所有帶@Configuration
的注解锈遥,所以就將我們的WebSecurityConfig
給加載了,這個(gè)config中矮湘,我們需要在configure(AuthenticationManagerBuilder auth)
方法中注冊(cè)一個(gè)繼承自UserDetailsService
的接口,這個(gè)接口中只有一個(gè)方法口糕,那就是使用username
獲取到數(shù)據(jù)庫中用戶信息并返回成UserDetail
實(shí)體缅阳。這個(gè)方法需要我們按照我們的不同業(yè)務(wù)場(chǎng)景重寫
WebSecurityConfig
/**
* @description:
* @author: coderymy
* @create: 2020-10-01 13:54
* <p>
* 1\. 創(chuàng)建WebSecurityConfig 類繼承WebSecurityConfigurerAdapter
* 2\. 類上加上@EnableWebSecurity,注解中包括@Configuration注解
* <p>
* WebSecurityConfigurerAdapter聲明了一些默認(rèn)的安全特性
* (1)驗(yàn)證所有的請(qǐng)求
* (2)可以使用springSecurity默認(rèn)的表單頁面進(jìn)行驗(yàn)證登錄
* (3)允許用戶使用http請(qǐng)求進(jìn)行驗(yàn)證
*/
/**
* 如何自定義認(rèn)證
* 1\. 實(shí)現(xiàn)并重寫configure(HttpSecurity http)方法景描,鑒權(quán)十办,也就是判斷該用戶是否有訪問該api的權(quán)限
* <p>
* <p>
* 頁面顯示403錯(cuò)誤,表示該用戶授權(quán)失敺(401代表該用戶認(rèn)證失旈俣础)前端可以使用返回的狀態(tài)碼來標(biāo)識(shí)如何給用戶展示
* 用2XX表示本次操作成功捌袜,用4XX表示是客戶端導(dǎo)致的失敗说搅,用5XX表示是服務(wù)器引起的錯(cuò)誤
*/
@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要求必須指定密碼加密方式,否則會(huì)在請(qǐng)求認(rèn)證的時(shí)候報(bào)錯(cuò)
* 同樣的虏等,如果指定了加密方式弄唧,就必須您的密碼在數(shù)據(jù)庫中存儲(chǔ)的是加密后的殖妇,才能比對(duì)成功
*
* @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()表單驗(yàn)證
* 3\. httpBasic()表單驗(yàn)證
* 4\. csrf()提供的跨站請(qǐng)求偽造防護(hù)功能
*/
/**
* 2\. authorizeRequests目的是指定url進(jìn)行攔截的疙教,也就是默認(rèn)這個(gè)url是“/”也就是所有的
* anyanyRequest()、antMatchers()和regexMatchers()三種方法來拼配系統(tǒng)的url畔况。并指定安全策略
*/
http.authorizeRequests()
//這里指定什么樣的接口地址的請(qǐng)求敦跌,需要什么樣的權(quán)限 ANT模式的URL匹配器
.antMatchers("/select/**").hasRole("USER")//用戶可以有查詢權(quán)限
.antMatchers("/insert/**").hasRole("ADMIN")//管理員可以有插入權(quán)限權(quán)限
.antMatchers("/empower/**").hasRole("SUPERADMIN")//超級(jí)管理員才有賦權(quán)的權(quán)限
.antMatchers("/login/**").permitAll()//標(biāo)識(shí)list所有權(quán)限都可以直接訪問澄干,即使不登錄也可以訪問。一般將login頁面放給這個(gè)權(quán)限
.and()
.formLogin()
// .loginProcessingUrl("/login/user")//用來定義什么樣的API請(qǐng)求時(shí)login請(qǐng)求
// .permitAll()//login請(qǐng)求需要是所有權(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;
/**
* 其實(shí)這樣就完成了認(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) {//用戶不存在報(bào)錯(cuò)
throw new UsernameNotFoundException("用戶不存在");
}
/**
* 將roles信息轉(zhuǎn)換成SpringSecurity內(nèi)部的形式麸俘,即Authorities
* commaSeparatedStringToAuthorityList可以將使用,隔開的角色列表切割出來并賦值List
* 如果不行的話,也可以自己實(shí)現(xiàn)這個(gè)方法惧笛,只要拆分出來就可以了
*/
//注意从媚,這里放入Authorities中的信息,都需要是以Role_開頭的患整,所以我們?cè)跀?shù)據(jù)庫中配置的都是這種格式的拜效。當(dāng)我們使用hasRole做比對(duì)的時(shí)候喷众,必須要是帶Role_開頭的。否則可以使用hasAuthority方法做比對(duì)
user.setAuthorities(AuthorityUtils.commaSeparatedStringToAuthorityList(user.getRoles()));
return user;
}
}
其實(shí)如果去掉上面的將自定義的JWT過濾器加入到過濾鏈中的話紧憾,這個(gè)認(rèn)證過程已經(jīng)完成了到千。使用下面的代碼就可以調(diào)用起整個(gè)認(rèn)證程序。
核心代碼
authenticate = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(userDto.getUsername(), userDto.getPassword()));
這一行就會(huì)將username和password放到認(rèn)證程序中進(jìn)行認(rèn)證赴穗。
也就是需要我們自己也邏輯讓他去觸發(fā)這個(gè)代碼的實(shí)現(xiàn)父阻。就可以自動(dòng)完成認(rèn)證程序了。就會(huì)觸發(fā)使用username獲取到數(shù)據(jù)庫用戶信息望抽,然后經(jīng)過密碼加密比對(duì)之后會(huì)將認(rèn)證結(jié)果返回加矛。
我們整合JWT其實(shí)也很簡(jiǎn)單,其實(shí)就是將JWT的登錄部分的操作煤篙,使用過濾器封裝斟览,將該過濾器放到整個(gè)認(rèn)證的過濾鏈中
2. 自定義JWT過濾器的配置
SpringSecurity過濾器的配置無非以下幾個(gè)條件
- 該過濾器過濾什么樣的API請(qǐng)求(也就是說什么樣的API請(qǐng)求會(huì)觸發(fā)該過濾器執(zhí)行)。配置被過濾的請(qǐng)求API
- 該過濾器做什么事
- 該過濾器執(zhí)行成功以及執(zhí)行失敗的各種情況該怎么做
- 該過濾器執(zhí)行的時(shí)機(jī)是什么樣的辑奈,也就是在過濾鏈之前還是之后執(zhí)行
先解決邏輯上以上三個(gè)問題的答案
- 我們需要攔截認(rèn)證請(qǐng)求苛茂,肯定是形如
xxx/login/xxx
這種API接口的請(qǐng)求啦 - 這個(gè)過濾器會(huì)做什么事呢?
- 首先鸠窗,我們需要進(jìn)行用戶名密碼的基礎(chǔ)驗(yàn)證妓羊,也就是合不合法
- 我們需要調(diào)用起
SpringSecurity
的默認(rèn)認(rèn)證程序 - 認(rèn)證程序執(zhí)行成功之后,我們需要按照用戶的信息以JWT的規(guī)則生成一個(gè)JWTToken并將其放入response中返回回去
- 認(rèn)證程序執(zhí)行失敗稍计,也就是用戶登錄失敗躁绸,我們也需要將返回信息封裝起來返回給用戶
- 執(zhí)行成功,我們需要返回給用戶JWTToken信息臣嚣。執(zhí)行失敗净刮,我們也要友好提示用戶
- 如果排除其他的業(yè)務(wù)場(chǎng)景干擾,目前過濾鏈只有進(jìn)行鑒權(quán)時(shí)候才使用硅则。所以針對(duì)不同的業(yè)務(wù)場(chǎng)景淹父,這個(gè)過濾器放的地方其實(shí)是不一樣的。(之后我們的另一個(gè)JWTToken校驗(yàn)的過濾器應(yīng)該需要在這個(gè)認(rèn)證的過濾器之后(兩個(gè)其實(shí)并不捕捉同樣的APi所以不會(huì)依次執(zhí)行怎虫。也不用太考慮這個(gè)問題))(我記得SpringCloud中的zuul網(wǎng)關(guān)的過濾器是可以自定義級(jí)別的暑认。但是目前在SpringSecurity中尚未發(fā)現(xiàn)這種功能)
針對(duì)以上解答,下面用代碼來做展示(ps:序號(hào)依次對(duì)應(yīng)上面)
- 配置過濾器過濾地址
/**
* 下面是為了配置這個(gè)Manager
* 配置其攔截的API請(qǐng)求地址
*
* @param authenticationManager
*/
public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
super.setFilterProcessesUrl("/login/user");//這里指定什么樣的API請(qǐng)求會(huì)被這個(gè)過濾器攔截
}
- 配置過濾器職能
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
//這里是定義一個(gè)攔截器大审,在認(rèn)證方面的攔截器蘸际,當(dāng)請(qǐng)求的時(shí)候回?cái)r截到這里面然后進(jìn)行身份認(rèn)證
//驗(yàn)證用戶名密碼是否正確之后
Authentication authenticate = null;
try {
System.out.println("InputStream:" + request.getInputStream());
UserDto userDto = new ObjectMapper().readValue(request.getInputStream(), UserDto.class);//這個(gè)地方相當(dāng)于封裝一下請(qǐng)求,因?yàn)榍芭_(tái)請(qǐng)求的是user.username="xxx"這種對(duì)象的形式饥努。
//對(duì)于這個(gè)過濾器攔截的接口捡鱼,去調(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 這個(gè)地方還有點(diǎn)問題缠诅,如果密碼錯(cuò)誤了,就會(huì)報(bào)BadCredentialsException錯(cuò)誤乍迄。需要看一下如果不讓這么報(bào)錯(cuò)并讓他進(jìn)入到unsuccessfulAuthentication方法中
} catch (BadCredentialsException e) {//捕捉密碼驗(yàn)證錯(cuò)誤異常
log.info("密碼錯(cuò)誤");
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)身份驗(yàn)證通過之后管引,會(huì)進(jìn)入這里,這里可以定義成功的返回
Users user = (Users) authResult.getPrincipal();//將principal中的信息轉(zhuǎn)換成User對(duì)象
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來定義褥伴,否則上述會(huì)失效的
}
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
//TODO 得看一下為啥會(huì)報(bào)這個(gè)錯(cuò)誤
System.out.println("認(rèn)證失敗");
ResponseUtil.write(response, JSONObject.toJSONString(ResultUtil.error("登錄失敗,賬號(hào)密碼錯(cuò)誤")));
}
- 執(zhí)行失敗與成功漾狼,分別是2中的
unsuccessfulAuthentication
和successfulAuthentication
方法 - 配置過濾鏈執(zhí)行的位置
在WebSecurityConfig中的configure(HttpSecurity http)方法中
JWTAuthenticationFilter jwtAuthenticationFilter = new JWTAuthenticationFilter(this.authenticationManager());
http.addFilterBefore(jwtAuthenticationFilter, JWTAuthenticationFilter.class);
完成了以上的配置重慢,前臺(tái)就可以使用/login/user
來進(jìn)行登錄操作了。登錄成功會(huì)返回一個(gè)JSON對(duì)象來供前端判斷成功與否
2. 代碼結(jié)果
全部代碼奉上逊躁,隨意寫的注釋有點(diǎn)多似踱,不看的可以給刪掉
- 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)驗(yàn)證所有的請(qǐng)求
* (2)可以使用springSecurity默認(rèn)的表單頁面進(jìn)行驗(yàn)證登錄
* (3)允許用戶使用http請(qǐng)求進(jìn)行驗(yàn)證
*/
/**
* 如何自定義認(rèn)證
* 1\. 實(shí)現(xiàn)并重寫configure(HttpSecurity http)方法稽煤,鑒權(quán)核芽,也就是判斷該用戶是否有訪問該api的權(quán)限
* <p>
* <p>
* 頁面顯示403錯(cuò)誤,表示該用戶授權(quán)失斀臀酢(401代表該用戶認(rèn)證失斣颉)前端可以使用返回的狀態(tài)碼來標(biāo)識(shí)如何給用戶展示
* 用2XX表示本次操作成功,用4XX表示是客戶端導(dǎo)致的失敗匾二,用5XX表示是服務(wù)器引起的錯(cuò)誤
*/
@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要求必須指定密碼加密方式哮独,否則會(huì)在請(qǐng)求認(rèn)證的時(shí)候報(bào)錯(cuò)
* 同樣的,如果指定了加密方式假勿,就必須您的密碼在數(shù)據(jù)庫中存儲(chǔ)的是加密后的借嗽,才能比對(duì)成功
*
* @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()表單驗(yàn)證
* 3\. httpBasic()表單驗(yàn)證
* 4\. csrf()提供的跨站請(qǐng)求偽造防護(hù)功能
*/
/**
* 2\. authorizeRequests目的是指定url進(jìn)行攔截的,也就是默認(rèn)這個(gè)url是“/”也就是所有的
* anyanyRequest()转培、antMatchers()和regexMatchers()三種方法來拼配系統(tǒng)的url。并指定安全策略
*/
http.authorizeRequests()
//這里指定什么樣的接口地址的請(qǐng)求浆竭,需要什么樣的權(quán)限 ANT模式的URL匹配器
.antMatchers("/select/**").hasRole("USER")//用戶可以有查詢權(quán)限
.antMatchers("/insert/**").hasRole("ADMIN")//管理員可以有插入權(quán)限權(quán)限
.antMatchers("/empower/**").hasRole("SUPERADMIN")//超級(jí)管理員才有賦權(quán)的權(quán)限
.antMatchers("/login/**").permitAll()//標(biāo)識(shí)list所有權(quán)限都可以直接訪問浸须,即使不登錄也可以訪問。一般將login頁面放給這個(gè)權(quán)限
.and()
.formLogin()
// .loginProcessingUrl("/login/user")//用來定義什么樣的API請(qǐng)求時(shí)login請(qǐng)求
// .permitAll()//login請(qǐng)求需要是所有權(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攔截器
* 這個(gè)定義完成之后邦泄,想要生效還需要將其加入到過濾鏈中
* @author: coderymy
* @create: 2020-10-02 09:36
**/
@Slf4j
public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
/**
* 驗(yàn)證用戶名密碼是否正確之后删窒,生成一個(gè)token,并將token返回客戶端
*
* @param request
* @param response
* @return
* @throws AuthenticationException
*/
private String secretKey = "ILoveDanChaoFan";
private AuthenticationManager authenticationManager;
/**
* 下面是為了配置這個(gè)Manager
* 配置其攔截的API請(qǐng)求地址
*
* @param authenticationManager
*/
public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
super.setFilterProcessesUrl("/login/user");//這里指定什么樣的API請(qǐng)求會(huì)被這個(gè)過濾器攔截
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
//這里是定義一個(gè)攔截器顺囊,在認(rèn)證方面的攔截器肌索,當(dāng)請(qǐng)求的時(shí)候回?cái)r截到這里面然后進(jìn)行身份認(rèn)證
//驗(yàn)證用戶名密碼是否正確之后
Authentication authenticate = null;
try {
System.out.println("InputStream:" + request.getInputStream());
UserDto userDto = new ObjectMapper().readValue(request.getInputStream(), UserDto.class);//這個(gè)地方相當(dāng)于封裝一下請(qǐng)求,因?yàn)榍芭_(tái)請(qǐng)求的是user.username="xxx"這種對(duì)象的形式特碳。
//對(duì)于這個(gè)過濾器攔截的接口诚亚,去調(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 這個(gè)地方還有點(diǎn)問題闸准,如果密碼錯(cuò)誤了,就會(huì)報(bào)BadCredentialsException錯(cuò)誤梢灭。需要看一下如果不讓這么報(bào)錯(cuò)并讓他進(jìn)入到unsuccessfulAuthentication方法中
} catch (BadCredentialsException e) {//捕捉密碼驗(yàn)證錯(cuò)誤異常
log.info("密碼錯(cuò)誤");
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)身份驗(yàn)證通過之后夷家,會(huì)進(jìn)入這里,這里可以定義成功的返回
Users user = (Users) authResult.getPrincipal();//將principal中的信息轉(zhuǎn)換成User對(duì)象
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來定義库快,否則上述會(huì)失效的
}
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
//TODO 得看一下為啥會(huì)報(bào)這個(gè)錯(cuò)誤
System.out.println("認(rèn)證失敗");
ResponseUtil.write(response, JSONObject.toJSONString(ResultUtil.error("登錄失敗,賬號(hào)密碼錯(cuò)誤")));
}
}
- 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;
/**
* 其實(shí)這樣就完成了認(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) {//用戶不存在報(bào)錯(cuò)
throw new UsernameNotFoundException("用戶不存在");
}
/**
* 將roles信息轉(zhuǎn)換成SpringSecurity內(nèi)部的形式缺谴,即Authorities
* commaSeparatedStringToAuthorityList可以將使用,隔開的角色列表切割出來并賦值List
* 如果不行的話,也可以自己實(shí)現(xiàn)這個(gè)方法耳鸯,只要拆分出來就可以了
*/
//注意湿蛔,這里放入Authorities中的信息,都需要是以Role_開頭的县爬,所以我們?cè)跀?shù)據(jù)庫中配置的都是這種格式的阳啥。當(dāng)我們使用hasRole做比對(duì)的時(shí)候,必須要是帶Role_開頭的财喳。否則可以使用hasAuthority方法做比對(duì)
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//這個(gè)注解可以幫助在entity中添加表中沒有的字段
private List<GrantedAuthority> authorities;
@Transient
private String jwtToken;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
//這個(gè)本身是對(duì)應(yīng)roles字段的察迟,但是因?yàn)榻Y(jié)構(gòu)不一致,所以重新創(chuàng)建一個(gè)耳高,后續(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.*;
/*
* 總的來說扎瓶,工具類中有三個(gè)方法
* 獲取JwtToken,獲取JwtToken中封裝的信息泌枪,判斷JwtToken是否存在
* 1\. encode()概荷,參數(shù)是=簽發(fā)人,存在時(shí)間碌燕,一些其他的信息=误证。返回值是JwtToken對(duì)應(yīng)的字符串
* 2\. decode(),參數(shù)是=JwtToken=修壕。返回值是荷載部分的鍵值對(duì)
* 3\. isVerify()愈捅,參數(shù)是=JwtToken=。返回值是這個(gè)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字符串包括三個(gè)部分
* 1\. header
* -當(dāng)前字符串的類型蓝谨,一般都是“JWT”
* -哪種算法加密,“HS256”或者其他的加密算法
* 所以一般都是固定的,沒有什么變化
* 2\. payload
* 一般有四個(gè)最常見的標(biāo)準(zhǔn)字段(下面有)
* iat:簽發(fā)時(shí)間譬巫,也就是這個(gè)jwt什么時(shí)候生成的
* jti:JWT的唯一標(biāo)識(shí)
* iss:簽發(fā)人咖楣,一般都是username或者userId
* exp:過期時(shí)間
*
* */
public String encode(String iss, long ttlMillis, Map<String, Object> claims) {
//iss簽發(fā)人,ttlMillis生存時(shí)間缕题,claims是指還想要在jwt中存儲(chǔ)的一些非隱私信息
if (claims == null) {
claims = new HashMap<>();
}
long nowMillis = System.currentTimeMillis();
JwtBuilder builder = Jwts.builder()
.setClaims(claims)
.setId(UUID.randomUUID().toString())//2\. 這個(gè)是JWT的唯一標(biāo)識(shí)截歉,一般設(shè)置成唯一的,這個(gè)方法可以生成唯一標(biāo)識(shí)
.setIssuedAt(new Date(nowMillis))//1\. 這個(gè)地方就是以毫秒為單位烟零,換算當(dāng)前系統(tǒng)時(shí)間生成的iat
.setSubject(iss)//3\. 簽發(fā)人瘪松,也就是JWT是給誰的(邏輯上一般都是username或者userId)
.signWith(signatureAlgorithm, base64EncodedSecretKey);//這個(gè)地方是生成jwt使用的算法和秘鑰
if (ttlMillis >= 0) {
long expMillis = nowMillis + ttlMillis;
Date exp = new Date(expMillis);//4\. 過期時(shí)間,這個(gè)也是使用毫秒生成的锨阿,使用當(dāng)前時(shí)間+前面?zhèn)魅氲某掷m(xù)時(shí)間生成
builder.setExpiration(exp);
}
return builder.compact();
}
//相當(dāng)于encode的方向宵睦,傳入jwtToken生成對(duì)應(yīng)的username和password等字段。Claim就是一個(gè)map
//也就是拿到荷載部分所有的鍵值對(duì)
public Claims decode(String jwtToken) {
// 得到 DefaultJwtParser
return Jwts.parser()
// 設(shè)置簽名的秘鑰
.setSigningKey(base64EncodedSecretKey)
// 設(shè)置需要解析的 jwt
.parseClaimsJws(jwtToken)
.getBody();
}
//判斷jwtToken是否合法
public boolean isVerify(String jwtToken) {
//這個(gè)是官方的校驗(yàn)規(guī)則墅诡,這里只寫了一個(gè)”校驗(yàn)算法“壳嚎,可以自己加
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); // 校驗(yàn)不通過會(huì)拋出異常
//判斷合法的標(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)系我郑趁,這個(gè)是基礎(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>