Spring Security代码实现JWT接口权限授予与校验功能
通过笔者前两篇文章的说明,相信大家已经知道JWT是什么,怎么用,该如何结合SpringSecurity使用。那么本节就用代码来具体的实现一下JWT登录认证及鉴权的流程。
一、环境准备工作
- 建立SpringBoot项目并集成了SpringSecurity,项目可以正常启动
- 通过controller写一个HTTP的GET方法服务接口,比如:“/hello”
- 实现最基本的动态数据验证及权限分配,即实现UserDetailsService接口和UserDetails接口。这两个接口都是向SpringSecurity提供用户、角色、权限等校验信息的接口
- 如果你学习过SpringSecurity的formLogin登录模式,请将HttpSecurity配置中的formLogin()配置段全部去掉。因为JWT完全使用JSON接口,没有from表单提交。
- HttpSecurity配置中一定要加上csrf().disable(),即暂时关掉跨站攻击CSRF的防御。这样是不安全的,我们后续章节再做处理。
以上的内容,我们在之前的文章中都已经讲过。如果仍然不熟悉,可以翻看本号之前的文章。
##二、开发JWT工具类
通过maven坐标引入JWT工具包jjwt
io.jsonwebtoken jjwt 0.9.0
在application.yml中加入如下自定义一些关于JWT的配置
jwt: header:JWTHeaderName secret:aabbccdd expiration:3600000
- 其中header是携带JWT令牌的HTTP的Header的名称。虽然我这里叫做JWTHeaderName,但是在实际生产中可读性越差越安全。
- secret是用来为JWT基础信息加密和解密的密钥。虽然我在这里在配置文件写死了,但是在实际生产中通常不直接写在配置文件里面。而是通过应用的启动参数传递,并且需要定期修改。
- expiration是JWT令牌的有效时间。
写一个SpringBoot配置自动加载的工具类。
@Data
@ConfigurationProperties(prefix="jwt")//配置自动加载,prefix是配置的前缀
@Component
publicclassJwtTokenUtilimplementsSerializable{
privateStringsecret;
privateLongexpiration;
privateStringheader;
/**
*生成token令牌
*
*@paramuserDetails用户
*@return令token牌
*/
publicStringgenerateToken(UserDetailsuserDetails){
Mapclaims=newHashMap<>(2);
claims.put("sub",userDetails.getUsername());
claims.put("created",newDate());
returngenerateToken(claims);
}
/**
*从令牌中获取用户名
*
*@paramtoken令牌
*@return用户名
*/
publicStringgetUsernameFromToken(Stringtoken){
Stringusername;
try{
Claimsclaims=getClaimsFromToken(token);
username=claims.getSubject();
}catch(Exceptione){
username=null;
}
returnusername;
}
/**
*判断令牌是否过期
*
*@paramtoken令牌
*@return是否过期
*/
publicBooleanisTokenExpired(Stringtoken){
try{
Claimsclaims=getClaimsFromToken(token);
Dateexpiration=claims.getExpiration();
returnexpiration.before(newDate());
}catch(Exceptione){
returnfalse;
}
}
/**
*刷新令牌
*
*@paramtoken原令牌
*@return新令牌
*/
publicStringrefreshToken(Stringtoken){
StringrefreshedToken;
try{
Claimsclaims=getClaimsFromToken(token);
claims.put("created",newDate());
refreshedToken=generateToken(claims);
}catch(Exceptione){
refreshedToken=null;
}
returnrefreshedToken;
}
/**
*验证令牌
*
*@paramtoken令牌
*@paramuserDetails用户
*@return是否有效
*/
publicBooleanvalidateToken(Stringtoken,UserDetailsuserDetails){
SysUseruser=(SysUser)userDetails;
Stringusername=getUsernameFromToken(token);
return(username.equals(user.getUsername())&&!isTokenExpired(token));
}
/**
*从claims生成令牌,如果看不懂就看谁调用它
*
*@paramclaims数据声明
*@return令牌
*/
privateStringgenerateToken(Mapclaims){
DateexpirationDate=newDate(System.currentTimeMillis()+expiration);
returnJwts.builder().setClaims(claims)
.setExpiration(expirationDate)
.signWith(SignatureAlgorithm.HS512,secret)
.compact();
}
/**
*从令牌中获取数据声明,如果看不懂就看谁调用它
*
*@paramtoken令牌
*@return数据声明
*/
privateClaimsgetClaimsFromToken(Stringtoken){
Claimsclaims;
try{
claims=Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
}catch(Exceptione){
claims=null;
}
returnclaims;
}
}
上面的代码就是使用io.jsonwebtoken.jjwt提供的方法开发JWT令牌生成、刷新的工具类。
三、开发登录接口(获取Token的接口)
"/authentication"接口用于登录验证,并且生成JWT返回给客户端
"/REFRESHTOKEN"接口用于刷新JWT,更新JWT令牌的有效期
@RESTCONTROLLER
PUBLICCLASSJWTAUTHCONTROLLER{
@RESOURCE
PRIVATEJWTAUTHSERVICEJWTAUTHSERVICE;
@POSTMAPPING(VALUE="/AUTHENTICATION")
PUBLICAJAXRESPONSELOGIN(@REQUESTBODYMAPMAP){
STRINGUSERNAME=MAP.GET("USERNAME");
STRINGPASSWORD=MAP.GET("PASSWORD");
IF(STRINGUTILS.ISEMPTY(USERNAME)||STRINGUTILS.ISEMPTY(PASSWORD)){
RETURNAJAXRESPONSE.ERROR(
NEWCUSTOMEXCEPTION(CUSTOMEXCEPTIONTYPE.USER_INPUT_ERROR,"用户名密码不能为空"));
}
RETURNAJAXRESPONSE.SUCCESS(JWTAUTHSERVICE.LOGIN(USERNAME,PASSWORD));
}
@POSTMAPPING(VALUE="/REFRESHTOKEN")
PUBLICAJAXRESPONSEREFRESH(@REQUESTHEADER("${JWT.HEADER}")STRINGTOKEN){
RETURNAJAXRESPONSE.SUCCESS(JWTAUTHSERVICE.REFRESHTOKEN(TOKEN));
}
}
核心的token业务逻辑写在JwtAuthService中
- login方法中首先使用用户名、密码进行登录验证。如果验证失败抛出BadCredentialsException异常。如果验证成功,程序继续向下走,生成JWT响应给前端
- refreshToken方法只有在JWTtoken没有过期的情况下才能刷新,过期了就不能刷新了。需要重新登录。
@Service
publicclassJwtAuthService{
@Resource
privateAuthenticationManagerauthenticationManager;
@Resource
privateUserDetailsServiceuserDetailsService;
@Resource
privateJwtTokenUtiljwtTokenUtil;
publicStringlogin(Stringusername,Stringpassword){
//使用用户名密码进行登录验证
UsernamePasswordAuthenticationTokenupToken=
newUsernamePasswordAuthenticationToken(username,password);
Authenticationauthentication=authenticationManager.authenticate(upToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
//生成JWT
UserDetailsuserDetails=userDetailsService.loadUserByUsername(username);
returnjwtTokenUtil.generateToken(userDetails);
}
publicStringrefreshToken(StringoldToken){
if(!jwtTokenUtil.isTokenExpired(oldToken)){
returnjwtTokenUtil.refreshToken(oldToken);
}
returnnull;
}
}
因为使用到了AuthenticationManager,所以在继承WebSecurityConfigurerAdapter的SpringSecurity配置实现类中,将AuthenticationManager声明为一个Bean。并将"/authentication"和"/refreshtoken"开放访问权限,如何开放访问权限,我们之前的文章已经讲过了。
@Bean(name=BeanIds.AUTHENTICATION_MANAGER)
@Override
publicAuthenticationManagerauthenticationManagerBean()throwsException{
returnsuper.authenticationManagerBean();
}
四、接口访问鉴权过滤器
当用户第一次登陆之后,我们将JWT令牌返回给了客户端,客户端应该将该令牌保存起来。在进行接口请求的时候,将令牌带上,放到HTTP的header里面,header的名字要和jwt.header的配置一致,这样服务端才能解析到。下面我们定义一个拦截器:
- 拦截接口请求,从请求request获取token,从token中解析得到用户名
- 然后通过UserDetailsService获得系统用户(从数据库、或其他其存储介质)
- 根据用户信息和JWT令牌,验证系统用户与用户输入的一致性,并判断JWT是否过期。如果没有过期,至此表明了该用户的确是该系统的用户。
- 但是,你是系统用户不代表你可以访问所有的接口。所以需要构造UsernamePasswordAuthenticationToken传递用户、权限信息,并将这些信息通过authentication告知SpringSecurity。SpringSecurity会以此判断你的接口访问权限。
@Slf4j
@Component
publicclassJwtAuthenticationTokenFilterextendsOncePerRequestFilter{
@Resource
privateMyUserDetailsServiceuserDetailsService;
@Resource
privateJwtTokenUtiljwtTokenUtil;
@Override
protectedvoiddoFilterInternal(HttpServletRequestrequest,
HttpServletResponseresponse,
FilterChainchain)throwsServletException,IOException{
//从这里开始获取request中的jwttoken
StringauthHeader=request.getHeader(jwtTokenUtil.getHeader());
log.info("authHeader:{}",authHeader);
//验证token是否存在
if(authHeader!=null&&StringUtils.isNotEmpty(authHeader)){
//根据token获取用户名
Stringusername=jwtTokenUtil.getUsernameFromToken(authHeader);
if(username!=null&&SecurityContextHolder.getContext().getAuthentication()==null){
//通过用户名获取用户的信息
UserDetailsuserDetails=this.userDetailsService.loadUserByUsername(username);
//验证JWT是否过期
if(jwtTokenUtil.validateToken(authHeader,userDetails)){
//加载用户、角色、权限信息,SpringSecurity根据这些信息判断接口的访问权限
UsernamePasswordAuthenticationTokenauthentication
=newUsernamePasswordAuthenticationToken(userDetails,null,
userDetails.getAuthorities());
authentication.setDetails(newWebAuthenticationDetailsSource()
.buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
chain.doFilter(request,response);
}
}
在springSecurity的配置类(即WebSecurityConfigurerAdapter实现类的configure(HttpSecurityhttp)配置方法中,加入如下配置:
.sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .addFilterBefore(jwtAuthenticationTokenFilter,UsernamePasswordAuthenticationFilter.class);
- 因为我们使用了JWT,表明了我们的应用是一个前后端分离的应用,所以我们可以开启STATELESS禁止使用session。
- 当然这并不绝对,前后端分离的应用通过一些办法也是可以使用session的,这不是本文的核心内容不做赘述。
- 将我们的自定义jwtAuthenticationTokenFilter,加载到UsernamePasswordAuthenticationFilter的前面。
五、测试一下:
测试登录接口,即:获取token的接口。输入正确的用户名、密码即可获取token。
下面我们访问一个我们定义的简单的接口“/hello”,但是不传递JWT令牌,结果是禁止访问。当我们将上一步返回的token,传递到header中,就能正常响应hello的接口结果。
总结
以上所述是小编给大家介绍的SpringSecurity代码实现JWT接口权限授予与校验功能,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对毛票票网站的支持!
如果你觉得本文对你有帮助,欢迎转载,烦请注明出处,谢谢!
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。