本篇博文主要是对请求授权操作代码实现,代码是基于上篇博文Springboot整合Spring Security 做JWT登录认证代码实现来实现的,大家可以先看上一篇登录认证再来看这篇可能会好理解一点。
访问授权流程
spring security访问授权主要流程图:
根据流程图所示,我们要实现的功能代码包含如下几部分:
- 有效授权认证信息类(Authentication)
- 请求拦截过滤器(Filter)
- 授权校验实现类(Provider)
- 授权成功处理器(SuccessHandler)
- 授权失败处理器(FailHandler)
- 授权校验配置类(Configure)
有效授权认证类代码实现
此功能代码主要是用于传输用户请求授权携带的认证信息。
public class JwtAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = 3981518947978158945L;
private UserDetails principal;
private String credentials;
private DecodedJWT token;
public JwtAuthenticationToken(DecodedJWT token) {
super(Collections.emptyList());
this.token = token;
}
public JwtAuthenticationToken(UserDetails principal, DecodedJWT token, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.token = token;
}
@Override
public void setDetails(Object details) {
super.setDetails(details);
this.setAuthenticated(true);
}
@Override
public Object getCredentials() {
return credentials;
}
@Override
public Object getPrincipal() {
return principal;
}
public DecodedJWT getToken() {
return token;
}
}
访问请求过滤器代码实现
过滤器主要实现功能如下:
(1)获取到请求携带的认证信息
(2)将认证信息交给相应的认证类进行认证
(3)根据认证结果指定相应的处理类处理
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private RequestMatcher authenticationRequestMatcher;
private List<RequestMatcher> permissiveRequestMatchers;
private AuthenticationManager authenticationManager;
private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();
//指定请求规则
public JwtAuthenticationFilter() {
this.authenticationRequestMatcher = new RequestHeaderRequestMatcher("Authorization");
}
@Override
public void afterPropertiesSet() throws ServletException {
Assert.notNull(authenticationManager, "authenticationManager must be specified");
Assert.notNull(successHandler, "AuthenticationSuccessHandler must be specified");
Assert.notNull(failureHandler, "AuthenticationFailureHandler must be specified");
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//判断请求头是否携带token
if (!authenticationRequestMatcher.matches(request)) {
filterChain.doFilter(request, response);
return;
}
Authentication authResult = null;
AuthenticationException failed = null;
try {
String jwtToken = StringUtils.removeStart(request.getHeader("Authorization"), "Bearer ");
if (StringUtils.isNotBlank(jwtToken)) {
//将请求携带的认证信息封装成我们用于授权的认证对象
JwtAuthenticationToken authenticationToken = new JwtAuthenticationToken(JWT.decode(jwtToken));
//认证信息校验开始
authResult = this.authenticationManager.authenticate(authenticationToken);
} else {
failed = new InsufficientAuthenticationException("token 不能为空");
}
} catch (JWTDecodeException e) {
logger.error("jwt format error", e);
failed = new InsufficientAuthenticationException("jwt format error", failed);
} catch (InternalAuthenticationServiceException e) {
logger.error(
"An internal error occurred while trying to authenticate the user.",
failed);
failed = e;
} catch (AuthenticationException e) {
// Authentication failed
failed = e;
}
if (authResult != null) {
successfulAuthentication(request, response, filterChain, authResult);
} else if (!permissiveRequest(request)) {
unsuccessfulAuthentication(request, response, failed);
return;
}
filterChain.doFilter(request, response);
}
/**
* 校验成功处理
*
* @param request
* @param response
* @param chain
* @param authResult
* @throws IOException
* @throws ServletException
*/
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {
SecurityContextHolder.getContext().setAuthentication(authResult);
successHandler.onAuthenticationSuccess(request, response, authResult);
}
/**
* 校验失败处理
*
* @param request
* @param response
* @param failed
* @throws IOException
* @throws ServletException
*/
protected void unsuccessfulAuthentication(HttpServletRequest request,
HttpServletResponse response, AuthenticationException failed)
throws IOException, ServletException {
SecurityContextHolder.clearContext();
failureHandler.onAuthenticationFailure(request, response, failed);
}
/**
* 授权失败,判断请求是否需要处理
*
* @param request
* @return
*/
protected boolean permissiveRequest(HttpServletRequest request) {
if (permissiveRequestMatchers == null) {
return false;
}
for (RequestMatcher permissiveMatcher : permissiveRequestMatchers) {
if (permissiveMatcher.matches(request)) {
return true;
}
}
return false;
}
/**
* 设置指定请求,如果请求授权失败也不影响该请求进行
*
* @param urls
* @return
*/
public void setPermissiveRequestMatchers(String... urls) {
if (permissiveRequestMatchers == null) {
permissiveRequestMatchers = new ArrayList<>();
}
for (String url : urls) {
permissiveRequestMatchers.add(new AntPathRequestMatcher(url));
}
}
public void setAuthenticationManager(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
public void setSuccessHandler(AuthenticationSuccessHandler successHandler) {
this.successHandler = successHandler;
}
public void setFailureHandler(AuthenticationFailureHandler failureHandler) {
this.failureHandler = failureHandler;
}
}
授权校验实现类代码实现
用户信息处理类(UserDetailsService实现类)
代码功能实现了基于auth0用户token的封装,用户登出逻辑处理。
@Component
public class SelfUserService implements UserDetailsService {
@Autowired
RedisTemplate<Object, Object> redisTemplate;
//token有效期,设为半个钟
public static final int EXPIRE_TIME = 1800000;
@Autowired
UserMapper userMapper;
/**
* 根据用户名获取用户信息
*
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userMapper.selectByName(username);
if (user == null) {
throw new UsernameNotFoundException("用户名不存在");
}
return org.springframework.security.core.userdetails.User.builder().username(username).password(user.getPassword()).roles("USER").build();
}
/**
* 将用户信息封装成用户认证jwt
*
* @param username
* @return
*/
public String getUserToken(String username) {
Date expireTime = new Date(System.currentTimeMillis() + EXPIRE_TIME);
String salt = getSalt(username);
String token = JWT.create().withSubject(username)
.withIssuedAt(new Date())
.withExpiresAt(expireTime)
.sign(Algorithm.HMAC256(salt));
return token;
}
/**
* 获取加密算法密钥
*
* @param username
* @return
*/
public String getSalt(String username){
//先从Redis中获取
String key = "salt" + username;
Object saltObj = redisTemplate.opsForValue().get(key);
String salt = "";
//如果Redis获取密钥失败,从数据库中获取
if(saltObj == null){
User user = userMapper.selectByName(username);
salt = user.getSalt();
}else {
salt = String.valueOf(saltObj);
}
//如果库里也没,重新生成密钥并将密钥更新至数据库和Redis中
if(StringUtils.isBlank(salt)){
salt = StringRandomUtil.generateString(15);
userMapper.refreshSalt(username,salt);
redisTemplate.opsForValue().set(key,salt);
}
return salt;
}
public void createUser(String username, String password) {
String encryptPwd = SelfPasswordEncod.encode(password);
/**
* @todo 保存用户名和加密后密码到数据库
*/
}
/**
* 账户退出,更新用户加密密钥
*
* @param username
*/
public void deleteUserLoginInfo(String username) {
String key = "salt" + username;
String salt = StringRandomUtil.generateString(15);
userMapper.refreshSalt(username,salt);
redisTemplate.opsForValue().set(key,salt);
}
}
授权校验实现类(AuthenticationProvider实现类)
@Component
public class JwtAuthenticationProvider implements AuthenticationProvider {
@Autowired
private SelfUserService selfUserService;
/**
* 认证主方法
*
* @param authentication
* @return
* @throws AuthenticationException
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
DecodedJWT jwt = ((JwtAuthenticationToken)authentication).getToken();
if(jwt.getExpiresAt().before(Calendar.getInstance().getTime())){
throw new NonceExpiredException("Token expires");
}
String username = jwt.getSubject();
UserDetails user = selfUserService.loadUserByUsername(username);
if(user == null || user.getPassword()==null){
throw new NonceExpiredException("Token expires");
}
//根据用户获取用户token加密密钥
String encryptSalt = selfUserService.getSalt(username);
try {
//校验用户信息,这里只是简单代码展示只校验参数用户名
Algorithm algorithm = Algorithm.HMAC256(encryptSalt);
JWTVerifier verifier = JWT.require(algorithm)
.withSubject(username)
.build();
verifier.verify(jwt.getToken());
} catch (Exception e) {
throw new BadCredentialsException("JWT token verify fail", e);
}
JwtAuthenticationToken token = new JwtAuthenticationToken(user, jwt, user.getAuthorities());
return token;
}
@Override
public boolean supports(Class<?> authentication) {
return authentication.isAssignableFrom(JwtAuthenticationToken.class);
}
}
授权成功处理类(AuthenticationSuccessHandler实现类)
授权成功,指定token刷新时间,判断token是否需要刷新,如果需要生成新的token传回前端。因为我们token设置的有效时长为半个钟,如果在这半个钟中用户有对系统请求访问,在一定时间内,我们需要更新其token。
@Component
public class JwtRefreshTokenHandler implements AuthenticationSuccessHandler {
//刷新间隔5分钟
private static final int tokenRefreshInterval = 300;
@Autowired
private SelfUserService jwtUserService;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
DecodedJWT jwt = ((JwtAuthenticationToken)authentication).getToken();
boolean shouldRefresh = shouldTokenRefresh(jwt.getIssuedAt());
if(shouldRefresh) {
String newToken = jwtUserService.getUserToken(((UserDetails)authentication.getPrincipal()).getUsername());
response.setHeader("Authorization", newToken);
}
}
protected boolean shouldTokenRefresh(Date issueAt){
LocalDateTime issueTime = LocalDateTime.ofInstant(issueAt.toInstant(), ZoneId.systemDefault());
return LocalDateTime.now().minusSeconds(tokenRefreshInterval).isAfter(issueTime);
}
}
授权校验配置类代码实现
JwtAuthenticationConfigure授权校验配置类:
@Component
public class JwtAuthenticationConfigure<T extends JwtAuthenticationConfigure<T,B>,B extends HttpSecurityBuilder<B>> extends AbstractHttpConfigurer<T,B> {
private JwtAuthenticationFilter authFilter;
public JwtAuthenticationConfigure() {
this.authFilter = new JwtAuthenticationFilter();
}
@Override
public void configure(B http) throws Exception {
authFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
authFilter.setFailureHandler(new LoginFailureHandler());
JwtAuthenticationFilter filter =postProcess(authFilter);
http.addFilterBefore(filter, LogoutFilter.class);
}
public JwtAuthenticationConfigure<T, B> permissiveRequestUrls(String ... urls){
authFilter.setPermissiveRequestMatchers(urls);
return this;
}
public JwtAuthenticationConfigure<T, B> tokenValidSuccessHandler(AuthenticationSuccessHandler successHandler){
authFilter.setSuccessHandler(successHandler);
return this;
}
}
整合授权校验配置类至web security配置中:
@Configuration
public class WebSecurityConfigure extends WebSecurityConfigurerAdapter {
@Autowired
private JsonLoginSuccessHandler jsonLoginSuccessHandler;
@Autowired
private JwtRefreshTokenHandler refreshTokenHandler;
@Autowired
private JwtAuthenticationProvider jwtAuthenticationProvider;
@Autowired
private UsernamePasswordAuthenticationProvider usernamePasswordAuthenticationProvider;
@Autowired
private TokenClearLogoutHandler tokenClearLogoutHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/image/**").permitAll()
.antMatchers("/record/**").hasRole("USER")
.and()
.csrf().disable()
.formLogin().disable()//禁用表单登录
.cors()
.and()
.apply(new LoginConfigure<>()).loginSuccessHandler(jsonLoginSuccessHandler)
.and()
.apply(new JwtAuthenticationConfigure<>()).tokenValidSuccessHandler(refreshTokenHandler).permissiveRequestUrls("/logout")
.and()
.logout()
//设置登出处理器,登出操作更新用户密钥
.addLogoutHandler(tokenClearLogoutHandler)
.logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler())
.and()
.sessionManagement().disable();//禁用session
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(usernamePasswordAuthenticationProvider)
.authenticationProvider(jwtAuthenticationProvider);
}
}
postman 测试
登录
请求预先写好的一个接口
将登录获取的token放到请求头中,属性名为Authorization,发送请求可见请求成功。
登出操作后再次请求预先写好的接口,请求失败,token校验失败。登出操作我们更新了用户加密密钥,导致再次请求时,token校验失败。
项目地址:https://gitee.com/huangjinfa/booking.git