SpringBoot集成SpringSecurity和JWT做登陸鑒權的實現
廢話
目前流行的前后端分離讓Java程序員可以更加專注的做好后臺業(yè)務邏輯的功能實現,提供如返回Json格式的數據接口就可以。SpringBoot的易用性和對其他框架的高度集成,用來快速開發(fā)一個小型應用是最佳的選擇。
一套前后端分離的后臺項目,剛開始就要面對的就是登陸和授權的問題。這里提供一套方案供大家參考。
主要看點:
- 登陸后獲取token,根據token來請求資源
- 根據用戶角色來確定對資源的訪問權限
- 統(tǒng)一異常處理
- 返回標準的Json格式數據
正文
首先是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 "
接著是對security的配置,讓security來保護我們的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
// 設置UserDetailsService
.userDetailsService(this.CustomUserDetailsService)
// 使用BCrypt進行密碼的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()
// 對于獲取token的rest api要允許匿名訪問
.antMatchers("/api/v1/auth", "/api/v1/signout", "/error/**", "/api/**").permitAll()
// 除上面外的所有請求全部需要鑒權認證
.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();
}
}
該類中配置了幾個bean來供security使用。
- JwtAuthenticationTokenFilter:token過濾器來驗證token有效性
- UserDetailsService:實現了DetailsService接口,用來做登陸驗證
- JwtAuthenticationEntryPoint :認證失敗處理類
- RestAuthenticationAccessDeniedHandler: 權限不足處理類
那么,接下來一個一個實現這些類:
/**
* token校驗,引用的stackoverflow一個答案里的處理方式
* 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ī)范,不允許通過驗證
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);
}
}
/**
* 認證失敗處理類,返回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 {
//驗證為未登陸狀態(tài)會進入此方法,認證錯誤
System.out.println("認證失?。? + 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();
}
}
因為我們使用的REST API,所以我們認為到達后臺的請求都是正常的,所以返回的HTTP狀態(tài)碼都是200,用接口返回的code來確定請求是否正常。
/**
* 權限不足處理類,返回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)下,權限不足執(zhí)行該方法
System.out.println("權限不足:" + 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();
}
}
/**
* 登陸身份認證
* 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) {
//用戶驗證
final Authentication authentication = authenticate(username, password);
//存儲認證信息
SecurityContextHolder.getContext().setAuthentication(authentication);
//生成token
final User user = (User) authentication.getPrincipal();
// User user = (User) userDetailsService.loadUserByUsername(username);
final String token = jwtTokenUtil.generateAccessToken(user);
//存儲token
jwtTokenUtil.putToken(username, token);
return new ResponseUserToken(token, user);
}
private Authentication authenticate(String username, String password) {
try {
//該方法會去調用userDetailsService.loadUserByUsername()去驗證用戶名和密碼,如果正確,則存儲該用戶名密碼到“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();
}
}
所有經controller轉發(fā)的請求拋出的自定義異常都會被捕獲處理,一般情況下就是返回給調用方一個json的報錯信息,包含自定義狀態(tài)碼、錯誤信息及補充描述信息。
值得注意的是,在請求到達controller之前,會被Filter攔截,如果在controller或者之前拋出的異常,自定義的異常處理器是無法處理的,需要自己重新定義一個全局異常處理器或者直接處理。
Filter攔截請求兩次的問題
跨域的post的請求會驗證兩次,get不會。網上的解釋是,post請求第一次是預檢請求,Request Method: OPTIONS。
解決方法:
在webSecurityConfig里添加
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
就可以不攔截options請求了。
這里只給出了最主要的代碼,還有controller層的訪問權限設置,返回狀態(tài)碼,返回類定義等等。
所有代碼已上傳GitHub,項目地址
到此這篇關于SpringBoot集成SpringSecurity和JWT做登陸鑒權的實現的文章就介紹到這了,更多相關SpringBoot JWT 登陸鑒權內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Springsecurity Oauth2如何設置token的過期時間
如果用戶在指定的時間內有操作就給token延長有限期,否則到期后自動過期,如何設置token的過期時間,本文就來詳細的介紹一下2021-08-08
Java語言實現簡單FTP軟件 FTP遠程文件管理模塊實現(10)
這篇文章主要為大家詳細介紹了Java語言實現簡單FTP軟件,FTP遠程文件管理模塊的實現方法,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-04-04
SpringBoot項目中的favicon.ico圖標無法顯示問題及解決
這篇文章主要介紹了SpringBoot項目中的favicon.ico圖標無法顯示問題及解決,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-01-01

