上篇文章讲了SpringSecurity的详细使用,这篇文章来说说SpringSecurity与Jwt结合来定制化认证过滤器,以此来改变SpringSecurity的默认认证方式。
首先了解一下Jwt是什么:
JWT (JSON Web Token) 是目前最流行的跨域认证解决方案,JWT 本身也是 Token,一种规范化之后的 JSON 结构的 Token。
JWT 自身包含了身份验证所需要的所有信息,因此,我们的服务器不需要存储 Session 信息。这显然增加了系统的可用性和伸缩性,大大减轻了服务端的压力。
JWT 认证可以有效避免 CSRF 攻击,因为 JWT 一般是存在在 localStorage 中,使用 JWT 进行身份验证的过程中是不会涉及到 Cookie 的。
Jwt本质上就是一个加密工具,下图是Jwt数据格式的三个部分:
Header : 描述 JWT 使用的加密算法以及 Token
的类型等。
Payload : 用来存放需要传递的数据(如:账号,过期时间 等)
Signature(签名) :组合Payload、Header 和一个密钥(Secret)来加密生成,默认是 (HMAC SHA256)生成。
JWT 加密三个部分后生成 xxxx.xxxx.xxxx 格式的字符串,三个部分之间以 .相隔
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Payload 也是 JSON 格式数据,其中包含了 Claims(声明,包含 JWT 的相关信息)。
Claims 分为三种类型(了解下面源码后能更好的理解三种类型):
Registered Claims(注册声明:默认)、Public Claims(公有声明:自定义)、Private Claims(私有声明:自定义)
Payload的注册声明的定义:
iss
(issuer):JWT 签发方。(本文未用)
iat
(issued at time):JWT 签发时间。(token生效时间)
sub
(subject):JWT 主题。(本文用来存放账号)
aud
(audience):JWT 接收方。(本文未用)
exp
(expiration time):JWT 的过期时间。(token过期时间)
nbf
(not before time):JWT 生效时间,早于该时间的 JWT 不能被接受处理。(本文未用)
jti
(JWT ID):JWT 唯一标识。
org.springframework.boot
spring-boot-starter-security
io.jsonwebtoken
jjwt
0.9.1
Payload本质上是一个Map集合,上面的命名表示Map集合默认键名,我们通过put()来定义参数(可自行查看源码):
注意:实现定制化认证过滤器只是认证,而授权还是交给Security框架实现。
下面来看看具体实现步骤(建议对照底部测试结果图来理解):
步骤1:
创建Jwt加密的工具类(拷贝即用):
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* JwtToken生成的工具类
* JWT token的格式:header.payload.signature
* header的格式(算法、token的类型):
* {"alg": "HS512","typ": "JWT"}
* payload的格式(用户名、创建时间、生成时间):
* {"sub":"wang","created":1489079981393,"exp":1489684781}
* signature的生成算法:
* HMACSHA512(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)
*/
@Component
@Slf4j
public class JwtTokenUtil {
private static final Logger logger = LoggerFactory.getLogger(JwtTokenUtil.class);
@Value("${jwt.secret}")
private String secret;// 密钥
@Value("${jwt.expiration}")
private Long expiration; // 定义过期时间
@Value("${jwt.tokenHead}")
private String tokenHead; // 自定义标识字符串
/**
* 根据用户信息生成token,其实只需传入账号,这里实际上只是将账号定义为了sub(主题)
*/
public String generateToken(UserDetails userDetails) {
logger.info("访问:JwtTokenUtil--generateToken()");
String token = generateJwtToken(userDetails);
return token;
}
/**
* 刷新token,这里实际上只是解析token获取账号,然后重新生成token并返回
*/
public String refreshToken(String token) {
logger.info("访问:JwtTokenUtil--refreshToken()");
Claims claims = getClaimsFromToken(token);
User user = new User();
user.setUsername(claims.getSubject());
token = generateJwtToken(user);
return token;
}
/**
* 判断传入参数是否为null来判断是要生成token还是刷新token
*/
private String generateJwtToken(UserDetails userDetails) {
logger.info("访问:JwtTokenUtil--generateJwtToken()");
// 构建Jwt的三个部分,使用的是设计模式里的建造者模式(Builder)
JwtBuilder jwtBuilder = Jwts.builder();
// Header部分:
jwtBuilder.setHeaderParam("type","JWT"); // 仅作声明
Map claims = new HashMap<>();
jwtBuilder.setClaims(claims); // claim: Playload部分,定义用当前容器来装载数据
jwtBuilder.setSubject(userDetails.getUsername()); // sub:这里将账号设置为主题了。
long iat = System.currentTimeMillis();
jwtBuilder.setIssuedAt(new Date(iat)); // iat:签发的时间(token生成时间)
jwtBuilder.setExpiration(new Date(iat + expiration)); // exp:过期时间
// Signature部分:
jwtBuilder.signWith(SignatureAlgorithm.HS512, secret); // Signature部分:指定算法进行签名,生成一个jws
String token = jwtBuilder.compact(); //构建JWT并将其序列化为一个紧凑的、url安全的字符串(token)
token = tokenHead +" "+ token; // 自定义标识字符串 拼接 token值
return token;
}
/**
* 从token中获取JWT中的负载(解析token值)
*/
private Claims getClaimsFromToken(String token) {
logger.info("访问:JwtTokenUtil--getClaimsFromToken()");
Claims claims = null;
try {
claims = Jwts.parser() // 开始解析token
.setSigningKey(secret) // 定义密钥(解密的盐)
.parseClaimsJws(token) // 解析token,解析失败抛出异常
.getBody(); // 获取Payload中数据
} catch (Exception e) {
logger.info("JWT格式验证失败:{}",token);
}
logger.info(claims+"");
return claims;
}
/**
* 从token中获取过期时间
*/
public Date getExpiredDateFromToken(String token) {
logger.info("访问:JwtTokenUtil--getExpiredDateFromToken()");
Claims claims = getClaimsFromToken(token);
return claims.getExpiration();
}
/**
* 从token中获取登录用户名()主题
*/
public String getUserNameFromToken(String token) {
logger.info("访问:JwtTokenUtil--getUserNameFromToken()");
String username;
try {
Claims claims = getClaimsFromToken(token);
username = claims.getSubject();
} catch (Exception e) {
username = null;
}
return username;
}
/**
* 判断token是否已经失效
*/
public boolean isTokenExpired(String token) {
logger.info("访问:JwtTokenUtil--isTokenExpired()");
if (StringUtils.isEmpty(token)){
return false;
}
Date expiredDate = getExpiredDateFromToken(token);
// Date判断过期时间是否在当前时间之前
return expiredDate.before(new Date());
}
}
步骤2:
GrantedAuthority:权限实体
/**
* 权限实体
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Authority implements GrantedAuthority, Serializable {
private static final long serialVersionUID = 5534135L;
private Integer id;
private String authorityName;
@Override
public String getAuthority() {
return authorityName;
}
}
步骤3
UserDetails:用户实体(关联权限)
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User implements UserDetails, Serializable {
private static final long serialVersionUID = -3694902274397865672L;
private Integer id;
private String username;
private String password;
private ArrayList authorities;
@Override
public ArrayList getAuthorities() {
return authorities;
}
@Override
public String getUsername() {
return username;
}
@Override
public String getPassword() {
return password;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
步骤4:
UserDetailsService:用作返回过滤器的认证和授权信息,传入的是username,这里主要是判断token中的用户名是否存在,不存在则返回null(即:认证失败),存在则表示认证成功,去获取用户名对应的所有权限,然后封装到UserDetails中并返回,剩下的授权操作就交给框架来完成。
@Service
public class AccountService implements UserDetailsService {
private static final Logger logger = LoggerFactory.getLogger(AccountService.class);
@Resource
private UserAndAuthorityServiceImpl userAndAuthorityService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
logger.info("访问:AccountService--loadUserByUsername()--值:"+username);
User user = userAndAuthorityService.getUserAndAuthority(username);
if (user==null){
throw new ServiceException(ErrorInformation.SERVICE_ERROR);
}
return user;
}
}
步骤5:
AccessDeniedHandler :过滤器检测到未登录时调用
// 过滤器检测到未登录时访问
@Component
public class NotLogInHandler implements AccessDeniedHandler {
@Autowired
ObjectMapper objectMapper;
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
LoggerFactory.getLogger(NotLogInHandler.class).info("访问:NotLogInHandler的handle()");
response.setCharacterEncoding("utf-8");
response.setContentType("application/json");
String s = objectMapper.writeValueAsString(D.failed(null,"未登录不能访问!",null));
PrintWriter writer = response.getWriter();
writer.print(s);
writer.flush();
}
}
步骤6:
AuthenticationEntryPoint :过滤器检测到未授权时调用
// 过滤器检测到未授权时访问
@Component
public class UnauthorizedHandler implements AuthenticationEntryPoint {
@Autowired
ObjectMapper objectMapper;
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
LoggerFactory.getLogger(NotLogInHandler.class).info("访问:UnauthorizedHandler的commence()");
response.setCharacterEncoding("utf-8");
response.setContentType("application/json");
String s = objectMapper.writeValueAsString(D.failed(null,"没有权限不能访问!",null));
PrintWriter writer = response.getWriter();
writer.print(s);
writer.flush();
}
}
步骤7:
OncePerRequestFilter :定制化过滤器,可以自定义认证规则
生成的token:
认证过滤器:
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationTokenFilter.class);
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Value("${jwt.httpHeader}")
private String httpHeader; // 请求头中token的键名
@Value("${jwt.tokenHead}")
private String tokenHead; // xxx为token标记(如:"xxx token..."),token值要单独拆解出来
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain
) throws ServletException, IOException {
// 从header中获取Authorization
String authHeader = request.getHeader(this.httpHeader);
// 判断 authHeader 不为空 并且以 bearer 开头
if (authHeader == null){
filterChain.doFilter(request,response);
return;
}
boolean isAuthizationHeader = StringUtils.startsWithIgnoreCase(authHeader,this.tokenHead);
if (!isAuthizationHeader){
filterChain.doFilter(request,response);
return;
}
// 截取 bearer 后面的字符串,两端去空格(获取token)
String authToken = authHeader.substring(this.tokenHead.length()).trim();
// 验证token是否过期
boolean tokenExpired = jwtTokenUtil.isTokenExpired(authToken);
// 从token中获取登录用户名(主题)
String username = jwtTokenUtil.getUserNameFromToken(authToken);
if (tokenExpired && StringUtils.isNullOrEmpty(username)){
filterChain.doFilter(request,response);
return;
}
logger.info("checking username:{}", username);
// 用户名不为空 并且SecurityContextHolder.getContext() 存储 权限的容器中没有相关权限则继续
boolean isNotAuthentication = SecurityContextHolder.getContext().getAuthentication() == null;
if (isNotAuthentication) {
// 从数据库读取用户信息
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
if (userDetails != null) {
// 将认证状态标记为已认证,一个带用户名和密码以及权限的Authentication(spring 自带的类)
UsernamePasswordAuthenticationToken authentication = null;
authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
// 从HttpServletRequest 对象,创建一个WebAuthenticationDetails对象
WebAuthenticationDetails details = new WebAuthenticationDetailsSource().buildDetails(request);
// 设置details
authentication.setDetails(details);
logger.info("authenticated user:{}", username);
// 存入本线程的安全容器 在访问接口拿到返回值后 要去主动清除 权限,避免干扰其他的线程
// SecurityContextHolder会把authentication放入到session里,供后面使用
SecurityContextHolder.getContext().setAuthentication(authentication);
// logger.info(jwtTokenUtil.);
// 刷新token失效时间
jwtTokenUtil.refreshToken(authToken);
}
}
filterChain.doFilter(request, response);
}
}
步骤8:
WebSecurityConfigurerAdapter :核心配置类,以上步骤完成后,必须将其配置到Security框架中,才能实现定制化认证过滤器。
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private AccountService accountService;
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Resource
private NotLogInHandler notLogInHandler;
@Resource
private UnauthorizedHandler unauthorizedHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// 配置控制器URL的角色和权限
.antMatchers("/admin/user/login","/admin/room/upload","/download","/admin/user/loginout").permitAll() // 放行路径
.anyRequest().authenticated() // 表示除了放行路径,其它路径都要认证授权
//Spring Security永远不会创建一个HttpSession,也永远不会使用它获取SecurityContext
.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
// 这一步,告诉Security 框架,我们要用自己的UserDetailsService实现类
// 来传递UserDetails对象给框架,框架会把这些信息生成Authorization对象使用
.and().userDetailsService(accountService);
// 在UsernamePasswordAuthenticationFilter过滤前,我们使用jwt进行过滤
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
//添加自定义未授权和未登录结果返回
http.exceptionHandling()
.accessDeniedHandler(notLogInHandler) // 未登录时访问
.authenticationEntryPoint(unauthorizedHandler); // 未授权时访问
http.cors();
// http.cors().disable(); // 关闭跨域, 默认开启
http.csrf().disable(); // 关闭请求伪造防护,用session就不用关
}
// 加密方式:这个要有,框架要对密码做加密,不能是明文,
// 也可以自定义加密方式实现PasswordEncoder接口再return自定义实现类
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
以上步骤只是实现了访问认证,下面是登陆校验的一种实现(仅作参考):
注意:要在上面配置类中配置放行路径。
@RestController
@RequestMapping("/admin/user")
public class UserAdmin{
@Autowired
private UserService userService;
@RequestMapping("/login")
public D login(
User entity,
HttpServletResponse response
){
User user = userService.login(entity,response);
return D.success(null,null,user);
}
}
@Service
@Transactional(propagation = Propagation.REQUIRED,isolation = Isolation.REPEATABLE_READ,timeout = 300,rollbackFor = Exception.class)
public class UserServiceImpl implements UserService {
private static final Logger logger = LoggerFactory.getLogger(UserServiceImpl.class);
@Resource
private UserDao mapper;
@Resource
private JwtTokenUtil jwtTokenUtil;
@Resource
private BCryptPasswordEncoder passwordEncoder; // 加密解密
// 登陆验证
@Override
public User login(User entity, HttpServletResponse response){
String rawPassword = entity.getPassword();
// 从数据库中查询用户
entity = mapper.selectOne(entity);
// 判断账号是否存在
if (entity == null) {
throw new LoginException(ErrorInformation.MESSAGE_LOGIN_FAILED);
}
// 判断密码是否正确
if (passwordEncoder.matches(rawPassword,entity.getPassword())){
throw new LoginException(ErrorInformation.MESSAGE_LOGIN_FAILED);
}
String token = jwtTokenUtil.generateToken(entity);
// 用户控制器带上token响应头,如不设置,前端无法接收响应头数据。
response.setHeader("Access-Control-Expose-Headers", "token");
// 设置token相应头
response.setHeader("token", token);
return entity;
}
// 退出登陆
@Override
public void loginout(HttpServletResponse response) {
response.setHeader("token",null);
}
测试结果:成功获取token后,可将token值复制到Jwt官网解析
Jwt官网链接:https://jwt.io