SpringBoot集成SpringSecurity和JWT做登陸鑒權(quán)的實(shí)現(xiàn)
廢話
目前流行的前后端分離讓Java程序員可以更加專注的做好后臺(tái)業(yè)務(wù)邏輯的功能實(shí)現(xiàn),提供如返回Json格式的數(shù)據(jù)接口就可以。SpringBoot的易用性和對(duì)其他框架的高度集成,用來快速開發(fā)一個(gè)小型應(yīng)用是最佳的選擇。
一套前后端分離的后臺(tái)項(xiàng)目,剛開始就要面對(duì)的就是登陸和授權(quán)的問題。這里提供一套方案供大家參考。
主要看點(diǎn):
- 登陸后獲取token,根據(jù)token來請(qǐng)求資源
- 根據(jù)用戶角色來確定對(duì)資源的訪問權(quán)限
- 統(tǒng)一異常處理
- 返回標(biāo)準(zhǔn)的Json格式數(shù)據(jù)
正文
首先是pom文件:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--這是不是必須,只是我引用了里面一些類的方法-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-solr</artifactId>
</dependency>
<!--這是不是必須,只是我引用了里面一些類的方法-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.6.1</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.6.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>1.0.9.RELEASE</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
application.yml:
spring : datasource : url : jdbc:mysql://127.0.0.1:3306/les_data_center?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&useAffectedRows=true&useSSL=false username : root password : 123456 driverClassName : com.mysql.jdbc.Driver jackson: data-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 mybatis : config-location : classpath:/mybatis-config.xml # JWT jwt: header: Authorization secret: mySecret #token有效期一天 expiration: 86400 tokenHead: "Bearer "
接著是對(duì)security的配置,讓security來保護(hù)我們的API
SpringBoot推薦使用配置類來代替xml配置。那這里,我也使用配置類的方式。
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private final JwtAuthenticationEntryPoint unauthorizedHandler;
private final AccessDeniedHandler accessDeniedHandler;
private final UserDetailsService CustomUserDetailsService;
private final JwtAuthenticationTokenFilter authenticationTokenFilter;
@Autowired
public WebSecurityConfig(JwtAuthenticationEntryPoint unauthorizedHandler,
@Qualifier("RestAuthenticationAccessDeniedHandler") AccessDeniedHandler accessDeniedHandler,
@Qualifier("CustomUserDetailsService") UserDetailsService CustomUserDetailsService,
JwtAuthenticationTokenFilter authenticationTokenFilter) {
this.unauthorizedHandler = unauthorizedHandler;
this.accessDeniedHandler = accessDeniedHandler;
this.CustomUserDetailsService = CustomUserDetailsService;
this.authenticationTokenFilter = authenticationTokenFilter;
}
@Autowired
public void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
authenticationManagerBuilder
// 設(shè)置UserDetailsService
.userDetailsService(this.CustomUserDetailsService)
// 使用BCrypt進(jìn)行密碼的hash
.passwordEncoder(passwordEncoder());
}
// 裝載BCrypt密碼編碼器
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.exceptionHandling().accessDeniedHandler(accessDeniedHandler).and()
// 由于使用的是JWT,我們這里不需要csrf
.csrf().disable()
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
// 基于token,所以不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
// 對(duì)于獲取token的rest api要允許匿名訪問
.antMatchers("/api/v1/auth", "/api/v1/signout", "/error/**", "/api/**").permitAll()
// 除上面外的所有請(qǐng)求全部需要鑒權(quán)認(rèn)證
.anyRequest().authenticated();
// 禁用緩存
httpSecurity.headers().cacheControl();
// 添加JWT filter
httpSecurity
.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/v2/api-docs",
"/swagger-resources/configuration/ui",
"/swagger-resources",
"/swagger-resources/configuration/security",
"/swagger-ui.html"
);
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
該類中配置了幾個(gè)bean來供security使用。
- JwtAuthenticationTokenFilter:token過濾器來驗(yàn)證token有效性
- UserDetailsService:實(shí)現(xiàn)了DetailsService接口,用來做登陸驗(yàn)證
- JwtAuthenticationEntryPoint :認(rèn)證失敗處理類
- RestAuthenticationAccessDeniedHandler: 權(quán)限不足處理類
那么,接下來一個(gè)一個(gè)實(shí)現(xiàn)這些類:
/**
* token校驗(yàn),引用的stackoverflow一個(gè)答案里的處理方式
* Author: JoeTao
* createAt: 2018/9/14
*/
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Value("${jwt.header}")
private String token_header;
@Resource
private JWTUtils jwtUtils;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
String auth_token = request.getHeader(this.token_header);
final String auth_token_start = "Bearer ";
if (StringUtils.isNotEmpty(auth_token) && auth_token.startsWith(auth_token_start)) {
auth_token = auth_token.substring(auth_token_start.length());
} else {
// 不按規(guī)范,不允許通過驗(yàn)證
auth_token = null;
}
String username = jwtUtils.getUsernameFromToken(auth_token);
logger.info(String.format("Checking authentication for user %s.", username));
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
User user = jwtUtils.getUserFromToken(auth_token);
if (jwtUtils.validateToken(auth_token, user)) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
logger.info(String.format("Authenticated user %s, setting security context", username));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
chain.doFilter(request, response);
}
}
/**
* 認(rèn)證失敗處理類,返回401
* Author: JoeTao
* createAt: 2018/9/20
*/
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable {
private static final long serialVersionUID = -8970718410437077606L;
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException {
//驗(yàn)證為未登陸狀態(tài)會(huì)進(jìn)入此方法,認(rèn)證錯(cuò)誤
System.out.println("認(rèn)證失?。? + authException.getMessage());
response.setStatus(200);
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
PrintWriter printWriter = response.getWriter();
String body = ResultJson.failure(ResultCode.UNAUTHORIZED, authException.getMessage()).toString();
printWriter.write(body);
printWriter.flush();
}
}
因?yàn)槲覀兪褂玫腞EST API,所以我們認(rèn)為到達(dá)后臺(tái)的請(qǐng)求都是正常的,所以返回的HTTP狀態(tài)碼都是200,用接口返回的code來確定請(qǐng)求是否正常。
/**
* 權(quán)限不足處理類,返回403
* Author: JoeTao
* createAt: 2018/9/21
*/
@Component("RestAuthenticationAccessDeniedHandler")
public class RestAuthenticationAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
//登陸狀態(tài)下,權(quán)限不足執(zhí)行該方法
System.out.println("權(quán)限不足:" + e.getMessage());
response.setStatus(200);
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
PrintWriter printWriter = response.getWriter();
String body = ResultJson.failure(ResultCode.FORBIDDEN, e.getMessage()).toString();
printWriter.write(body);
printWriter.flush();
}
}
/**
* 登陸身份認(rèn)證
* Author: JoeTao
* createAt: 2018/9/14
*/
@Component(value="CustomUserDetailsService")
public class CustomUserDetailsService implements UserDetailsService {
private final AuthMapper authMapper;
public CustomUserDetailsService(AuthMapper authMapper) {
this.authMapper = authMapper;
}
@Override
public User loadUserByUsername(String name) throws UsernameNotFoundException {
User user = authMapper.findByUsername(name);
if (user == null) {
throw new UsernameNotFoundException(String.format("No user found with username '%s'.", name));
}
Role role = authMapper.findRoleByUserId(user.getId());
user.setRole(role);
return user;
}
}
登陸邏輯:
public ResponseUserToken login(String username, String password) {
//用戶驗(yàn)證
final Authentication authentication = authenticate(username, password);
//存儲(chǔ)認(rèn)證信息
SecurityContextHolder.getContext().setAuthentication(authentication);
//生成token
final User user = (User) authentication.getPrincipal();
// User user = (User) userDetailsService.loadUserByUsername(username);
final String token = jwtTokenUtil.generateAccessToken(user);
//存儲(chǔ)token
jwtTokenUtil.putToken(username, token);
return new ResponseUserToken(token, user);
}
private Authentication authenticate(String username, String password) {
try {
//該方法會(huì)去調(diào)用userDetailsService.loadUserByUsername()去驗(yàn)證用戶名和密碼,如果正確,則存儲(chǔ)該用戶名密碼到“security 的 context中”
return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
} catch (DisabledException | BadCredentialsException e) {
throw new CustomException(ResultJson.failure(ResultCode.LOGIN_ERROR, e.getMessage()));
}
}
自定義異常:
@Getter
public class CustomException extends RuntimeException{
private ResultJson resultJson;
public CustomException(ResultJson resultJson) {
this.resultJson = resultJson;
}
}
統(tǒng)一異常處理:
/**
* 異常處理類
* controller層異常無法捕獲處理,需要自己處理
* Created by jt on 2018/8/27.
*/
@RestControllerAdvice
@Slf4j
public class DefaultExceptionHandler {
/**
* 處理所有自定義異常
* @param e
* @return
*/
@ExceptionHandler(CustomException.class)
public ResultJson handleCustomException(CustomException e){
log.error(e.getResultJson().getMsg().toString());
return e.getResultJson();
}
}
所有經(jīng)controller轉(zhuǎn)發(fā)的請(qǐng)求拋出的自定義異常都會(huì)被捕獲處理,一般情況下就是返回給調(diào)用方一個(gè)json的報(bào)錯(cuò)信息,包含自定義狀態(tài)碼、錯(cuò)誤信息及補(bǔ)充描述信息。
值得注意的是,在請(qǐng)求到達(dá)controller之前,會(huì)被Filter攔截,如果在controller或者之前拋出的異常,自定義的異常處理器是無法處理的,需要自己重新定義一個(gè)全局異常處理器或者直接處理。
Filter攔截請(qǐng)求兩次的問題
跨域的post的請(qǐng)求會(huì)驗(yàn)證兩次,get不會(huì)。網(wǎng)上的解釋是,post請(qǐng)求第一次是預(yù)檢請(qǐng)求,Request Method: OPTIONS。
解決方法:
在webSecurityConfig里添加
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
就可以不攔截options請(qǐng)求了。
這里只給出了最主要的代碼,還有controller層的訪問權(quán)限設(shè)置,返回狀態(tài)碼,返回類定義等等。
所有代碼已上傳GitHub,項(xiàng)目地址
到此這篇關(guān)于SpringBoot集成SpringSecurity和JWT做登陸鑒權(quán)的實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)SpringBoot JWT 登陸鑒權(quán)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- SpringBoot整合SpringSecurity和JWT的示例
- SpringBoot3.0+SpringSecurity6.0+JWT的實(shí)現(xiàn)
- Springboot WebFlux集成Spring Security實(shí)現(xiàn)JWT認(rèn)證的示例
- 詳解SpringBoot+SpringSecurity+jwt整合及初體驗(yàn)
- SpringBoot集成Spring security JWT實(shí)現(xiàn)接口權(quán)限認(rèn)證
- SpringBoot3.x接入Security6.x實(shí)現(xiàn)JWT認(rèn)證的完整步驟
- SpringBoot+SpringSecurity+jwt實(shí)現(xiàn)驗(yàn)證
- SpringBoot Security+JWT簡(jiǎn)單搭建的實(shí)現(xiàn)示例
相關(guān)文章
Java如何檢測(cè)當(dāng)前CPU負(fù)載狀態(tài)
在Java中,直接檢測(cè)CPU負(fù)載狀態(tài)并不像在操作系統(tǒng)命令行中那樣簡(jiǎn)單,因?yàn)镴ava標(biāo)準(zhǔn)庫并沒有直接提供這樣的功能,這篇文章主要介紹了java檢測(cè)當(dāng)前CPU負(fù)載狀態(tài)的方法,需要的朋友可以參考下2024-06-06
Springsecurity Oauth2如何設(shè)置token的過期時(shí)間
如果用戶在指定的時(shí)間內(nèi)有操作就給token延長有限期,否則到期后自動(dòng)過期,如何設(shè)置token的過期時(shí)間,本文就來詳細(xì)的介紹一下2021-08-08
Java語言實(shí)現(xiàn)簡(jiǎn)單FTP軟件 FTP遠(yuǎn)程文件管理模塊實(shí)現(xiàn)(10)
這篇文章主要為大家詳細(xì)介紹了Java語言實(shí)現(xiàn)簡(jiǎn)單FTP軟件,F(xiàn)TP遠(yuǎn)程文件管理模塊的實(shí)現(xiàn)方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-04-04
Springboot實(shí)現(xiàn)ENC加密的詳細(xì)流程
在項(xiàng)目開發(fā)過程中,需要配置數(shù)據(jù)庫連接密碼、Redis密碼、網(wǎng)盤上傳的AK/SK等敏感信息,都需要保存在配置文件里,或者配置中心,這些信息如果泄露,還是會(huì)造成一定的困擾,下面這篇文章主要給大家介紹了關(guān)于Springboot實(shí)現(xiàn)ENC加密的詳細(xì)流程,需要的朋友可以參考下2023-06-06
java實(shí)現(xiàn)飛機(jī)大戰(zhàn)小游戲
這篇文章主要為大家詳細(xì)介紹了java實(shí)現(xiàn)飛機(jī)大戰(zhàn)小游戲,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-06-06
java實(shí)現(xiàn)的xml格式化實(shí)現(xiàn)代碼
這篇文章主要介紹了java實(shí)現(xiàn)的xml格式化實(shí)現(xiàn)代碼,需要的朋友可以參考下2016-11-11
SpringBoot使用CORS實(shí)現(xiàn)無縫跨域的方法實(shí)現(xiàn)
CORS 是一種在服務(wù)端設(shè)置響應(yīng)頭部信息的機(jī)制,允許特定的源進(jìn)行跨域訪問,本文主要介紹了SpringBoot使用CORS實(shí)現(xiàn)無縫跨域的方法實(shí)現(xiàn),具有一定的參考價(jià)值,感興趣的可以了解一下2023-10-10
SpringBoot項(xiàng)目中的favicon.ico圖標(biāo)無法顯示問題及解決
這篇文章主要介紹了SpringBoot項(xiàng)目中的favicon.ico圖標(biāo)無法顯示問題及解決,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-01-01

