SpringBoot Security+JWT简单搭建的实现示例
作者:现在没有牛仔了
SpringBoot Security是Spring官方提供的一个安全框架,他的核心功能是对系统用户进行认证和鉴权,也经常在项目中被使用到,本文不介绍其太过深入的内容,只介绍如何实现并完成认证和鉴权的测试。主要分三步来实现:
- 配置JWT
- 配置Security
- 编写测试相关代码
首先创建一个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相关配置
jwt:
expiration: 3600000 //token过期时间,1个小时
tokenHeader: Authorization //token在header中的属性名
secret: jwt-token-secret //生成token的密钥
创建jwt工具类,方便实现根据用户信息生成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;
//根据用户信息生成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的实现类
@Data
public class SysUser {
private Integer id;
private String username;
private String password;
}
@Data
@EqualsAndHashCode
@Accessors(chain = true) //实现链式set方法
public class SecurityUserDetails extends SysUser implements UserDetails {
//权限列表
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;
}
}
提示
因为只是记录一下如何实现security+jwt,所以没有从数据库中读取真实的用户信息,而是直接将用户信息和权限信息写死测试。
重写UserDetailsService的loadUserByUsername方法实现具体的认证授权逻辑
@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);
}
}
提示
这里直接把用户的权限写死,ROLE_USER表示用户拥有USER权限,因为权限都是以ROLE_开头的。
紧接着创建一个用户请求的过滤器,用来拦截用户请求,分析用户有没有该请求的权限
@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上下文中存储用户及用户的权限的信息
实现AuthenticationEntryPoint接口的commence方法,当请求没有携带认证信息或者说认证失败时,使用我们自己编写的处理逻辑。
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
}
}
提示
如果请求没有携带认证信息或者说认证失败时,会返回给客户端401,如果不重写commence方法,默认返回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();
}
/**
* 认证逻辑配置
*/
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(jwtUserDetailsService).passwordEncoder(passwordEncoder);
}
}
提示
上面的代码中,.exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint)表示使用自定义的认证失败处理逻辑。并且配置类中,自定义了用户密码的加密方式,configureGlobal方法设置自定义的loadUserByUsername方法实现和校验密码校验的加密方式。
编写测试相关代码
编写一个不需要认证授权就能访问的登录接口/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权限的接口/sys/testUser
@RestController
@RequestMapping("/sys")
public class SysUserController {
@PreAuthorize("hasAnyRole('USER')")
@PostMapping(value = "/testUser")
public String testNeed() {
return "hello world";
}
}
测试
启动SpringBoot项目,对上面的接口进行测试,首先调用/login接口登录并获取token

请求成功并获取到jwt生成的token。紧接着调用需要USER权限的/testUser,请求时要在请求头里面携带token

请求成功!
现在来测试一下失败的情况,不传token直接请求

请求失败,返回401,表示没有认证。再来测试一下如果将@PreAuthorize("hasAnyRole('USER')")中的权限改为Admin,然后用刚刚生成的token去请求
@PreAuthorize("hasAnyRole('Admin')")
@PostMapping(value = "/testUser")
public String testNeed() {
return "hello world";
}

由于token中包含的授权信息是USER,所以将@PreAuthorize("hasAnyRole('USER')")中的USER改为Admin后,返回了403,表示没有这个权限。
到此这篇关于SpringBoot Security+JWT简单搭建的实现示例的文章就介绍到这了,更多相关SpringBoot Security JWT搭建内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
您可能感兴趣的文章:
- SpringBoot整合SpringSecurity和JWT的示例
- SpringBoot集成SpringSecurity和JWT做登陆鉴权的实现
- SpringBoot3.0+SpringSecurity6.0+JWT的实现
- Springboot WebFlux集成Spring Security实现JWT认证的示例
- 详解SpringBoot+SpringSecurity+jwt整合及初体验
- SpringBoot集成Spring security JWT实现接口权限认证
- SpringBoot3.x接入Security6.x实现JWT认证的完整步骤
- SpringBoot+SpringSecurity+jwt实现验证
