SpringBoot Security+JWT簡單搭建的實現(xiàn)示例
SpringBoot Security是Spring官方提供的一個安全框架,他的核心功能是對系統(tǒng)用戶進(jìn)行認(rèn)證和鑒權(quán),也經(jīng)常在項目中被使用到,本文不介紹其太過深入的內(nèi)容,只介紹如何實現(xiàn)并完成認(rèn)證和鑒權(quán)的測試。主要分三步來實現(xiàn):
- 配置JWT
- 配置Security
- 編寫測試相關(guān)代碼
首先創(chuàng)建一個springboot項目,我的版本是2.6.13,依然是java8,整合Security+JWT需要用到的Maven依賴如下:
<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>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
配置JWT
先在yml配置文件中添加jwt相關(guān)配置
jwt:
expiration: 3600000 //token過期時間,1個小時
tokenHeader: Authorization //token在header中的屬性名
secret: jwt-token-secret //生成token的密鑰
創(chuàng)建jwt工具類,方便實現(xiàn)根據(jù)用戶信息生成token,以及通過token中獲取用戶信息
@Component
@Data
public class JwtTokenUtil implements Serializable {
private static final long serialVersionUID = -3301605591108950415L;
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private Long expiration;
private Clock clock = DefaultClock.INSTANCE;
//根據(jù)用戶信息生成token
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
return doGenerateToken(claims, userDetails.getUsername());
}
private String doGenerateToken(Map<String, Object> claims, String subject) {
final Date createdDate = clock.now();
final Date expirationDate = calculateExpirationDate(createdDate);
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(createdDate)
.setExpiration(expirationDate)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
private Date calculateExpirationDate(Date createdDate) {
return new Date(createdDate.getTime() + expiration);
}
public Boolean validateToken(String token, UserDetails userDetails) {
SecurityUserDetails user = (SecurityUserDetails) userDetails;
final String username = getUsernameFromToken(token);
return (username.equals(user.getUsername())
&& !isTokenExpired(token)
);
}
//通過token獲取用戶名username
public String getUsernameFromToken(String token) {
return getClaimFromToken(token, Claims::getSubject);
}
public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
final Claims claims = getAllClaimsFromToken(token);
return claimsResolver.apply(claims);
}
private Claims getAllClaimsFromToken(String token) {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
}
private Boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(clock.now());
}
public Date getExpirationDateFromToken(String token) {
return getClaimFromToken(token, Claims::getExpiration);
}
}
配置Security
編寫一個存儲用戶信息的UserDetails的實現(xiàn)類
@Data
public class SysUser {
private Integer id;
private String username;
private String password;
}
@Data
@EqualsAndHashCode
@Accessors(chain = true) //實現(xiàn)鏈?zhǔn)絪et方法
public class SecurityUserDetails extends SysUser implements UserDetails {
//權(quán)限列表
private Collection<? extends GrantedAuthority> authorities;
public SecurityUserDetails(String userName,Collection<? extends GrantedAuthority> authorities){
this.setUsername(userName);
String encode = new BCryptPasswordEncoder().encode("123456");
this.setPassword(encode);
this.setAuthorities(authorities);
}
/**
* 下面這些都返回true
* @return
*/
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
提示
因為只是記錄一下如何實現(xiàn)security+jwt,所以沒有從數(shù)據(jù)庫中讀取真實的用戶信息,而是直接將用戶信息和權(quán)限信息寫死測試。
重寫UserDetailsService的loadUserByUsername方法實現(xiàn)具體的認(rèn)證授權(quán)邏輯
@Service
public class JwtUserDetailsServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
List<GrantedAuthority> authorityList = new ArrayList<>();
authorityList.add(new SimpleGrantedAuthority("ROLE_USER"));
return new SecurityUserDetails(username,authorityList);
}
}
提示
這里直接把用戶的權(quán)限寫死,ROLE_USER表示用戶擁有USER權(quán)限,因為權(quán)限都是以ROLE_開頭的。
緊接著創(chuàng)建一個用戶請求的過濾器,用來攔截用戶請求,分析用戶有沒有該請求的權(quán)限
@Component
public class JwtAuthorizationTokenFilter extends OncePerRequestFilter {
private final UserDetailsService userDetailsService;
private final JwtTokenUtil jwtTokenUtil;
private final String tokenHeader;
public JwtAuthorizationTokenFilter(@Qualifier("jwtUserDetailsServiceImpl") UserDetailsService userDetailsService,
JwtTokenUtil jwtTokenUtil,
@Value("${jwt.tokenHeader}") String tokenHeader){
this.userDetailsService = userDetailsService;
this.jwtTokenUtil = jwtTokenUtil;
this.tokenHeader = tokenHeader;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
final String requestHeader = request.getHeader(this.tokenHeader);
String username = null;
String authToken = null;
if(requestHeader != null && requestHeader.startsWith("Bearer ")){
authToken = requestHeader.substring(7);
try {
username = jwtTokenUtil.getUsernameFromToken(authToken);
}catch (ExpiredJwtException e){
e.printStackTrace();
}
}
if(username!=null&& SecurityContextHolder.getContext().getAuthentication() == null){
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
if(jwtTokenUtil.validateToken(authToken,userDetails)){
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails,null,userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
filterChain.doFilter(request,response);
}
}
提示Bearer 必須帶空格,第二個if判斷就是為了加載到用戶的信息,并且在Security上下文中存儲用戶及用戶的權(quán)限的信息
實現(xiàn)AuthenticationEntryPoint接口的commence方法,當(dāng)請求沒有攜帶認(rèn)證信息或者說認(rèn)證失敗時,使用我們自己編寫的處理邏輯。
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
}
}
提示
如果請求沒有攜帶認(rèn)證信息或者說認(rèn)證失敗時,會返回給客戶端401,如果不重寫commence方法,默認(rèn)返回403
接下來編寫Security的核心配置類,重寫WebSecurityConfigurerAdapter中的configure方法
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
@Autowired
JwtUserDetailsServiceImpl jwtUserDetailsService;
@Autowired
JwtAuthorizationTokenFilter authenticationTokenFilter;
@Autowired
@Lazy
PasswordEncoder passwordEncoder;
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint)
.and()
.authorizeRequests()
.antMatchers("/login").permitAll()
.antMatchers(HttpMethod.OPTIONS, "/**").anonymous()
.anyRequest().authenticated() //除上面以外的都攔截
.and()
.csrf().disable() //禁用security自帶的跨域處理
//讓Security不使用session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
@Bean
public PasswordEncoder passwordEncoderBean() {
return new BCryptPasswordEncoder();
}
/**
* 認(rèn)證邏輯配置
*/
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(jwtUserDetailsService).passwordEncoder(passwordEncoder);
}
}
提示
上面的代碼中,.exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint)表示使用自定義的認(rèn)證失敗處理邏輯。并且配置類中,自定義了用戶密碼的加密方式,configureGlobal方法設(shè)置自定義的loadUserByUsername方法實現(xiàn)和校驗密碼校驗的加密方式。
編寫測試相關(guān)代碼
編寫一個不需要認(rèn)證授權(quán)就能訪問的登錄接口/login
@RestController
public class LoginController {
@Autowired
@Qualifier("jwtUserDetailsServiceImpl")
private UserDetailsService userDetailsService;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@PostMapping("/login")
public String login(@RequestBody SysUser sysUser, HttpServletRequest request){
final UserDetails userDetails = userDetailsService.loadUserByUsername(sysUser.getUsername());
final String token = jwtTokenUtil.generateToken(userDetails);
return token;
}
}
編寫一個需要USER權(quán)限的接口/sys/testUser
@RestController
@RequestMapping("/sys")
public class SysUserController {
@PreAuthorize("hasAnyRole('USER')")
@PostMapping(value = "/testUser")
public String testNeed() {
return "hello world";
}
}
測試
啟動SpringBoot項目,對上面的接口進(jìn)行測試,首先調(diào)用/login接口登錄并獲取token

請求成功并獲取到jwt生成的token。緊接著調(diào)用需要USER權(quán)限的/testUser,請求時要在請求頭里面攜帶token

請求成功!
現(xiàn)在來測試一下失敗的情況,不傳token直接請求

請求失敗,返回401,表示沒有認(rèn)證。再來測試一下如果將@PreAuthorize("hasAnyRole('USER')")中的權(quán)限改為Admin,然后用剛剛生成的token去請求
@PreAuthorize("hasAnyRole('Admin')")
@PostMapping(value = "/testUser")
public String testNeed() {
return "hello world";
}

由于token中包含的授權(quán)信息是USER,所以將@PreAuthorize("hasAnyRole('USER')")中的USER改為Admin后,返回了403,表示沒有這個權(quán)限。
到此這篇關(guān)于SpringBoot Security+JWT簡單搭建的實現(xiàn)示例的文章就介紹到這了,更多相關(guān)SpringBoot Security JWT搭建內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- SpringBoot整合SpringSecurity和JWT的示例
- SpringBoot集成SpringSecurity和JWT做登陸鑒權(quán)的實現(xiàn)
- SpringBoot3.0+SpringSecurity6.0+JWT的實現(xiàn)
- Springboot WebFlux集成Spring Security實現(xiàn)JWT認(rèn)證的示例
- 詳解SpringBoot+SpringSecurity+jwt整合及初體驗
- SpringBoot集成Spring security JWT實現(xiàn)接口權(quán)限認(rèn)證
- SpringBoot3.x接入Security6.x實現(xiàn)JWT認(rèn)證的完整步驟
- SpringBoot+SpringSecurity+jwt實現(xiàn)驗證
相關(guān)文章
springboot?使用clickhouse實時大數(shù)據(jù)分析引擎(使用方式)
這篇文章主要介紹了springboot?使用clickhouse實時大數(shù)據(jù)分析引擎的方法,本文通過實例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友參考下吧2022-02-02
Java利用位運算實現(xiàn)比較兩個數(shù)的大小
這篇文章主要為大家介紹了,在Java中如何不用任何比較判斷符(>,==,<),返回兩個數(shù)( 32 位整數(shù))中較大的數(shù),感興趣的可以了解一下2022-08-08
在java中 利用匿名內(nèi)部類進(jìn)行較簡潔的雙括弧初始化的方法
本篇文章小編將為大家介紹,關(guān)于在java中 利用匿名內(nèi)部類進(jìn)行較簡潔的雙括弧初始化的方法,有需要的朋友可以參考一下2013-04-04
Spring Data JPA 設(shè)置字段默認(rèn)值方式
這篇文章主要介紹了Spring Data JPA設(shè)置字段默認(rèn)值方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-11-11
SpringBoot啟動流程SpringApplication準(zhǔn)備階段源碼分析
這篇文章主要為大家介紹了SpringBoot啟動流程SpringApplication準(zhǔn)備階段源碼分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-04-04

