本篇文章将教大家在 shiro + springBoot 的基础上整合 JJWT (JSON Web Token)
JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用 JWT 在用户和服务器之间传递安全可靠的信息。
我们利用一定的编码生成 Token,并在 Token 中加入一些非敏感信息,将其传递。
一个完整的 Token :
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcm9tX3VzZXIiOiJCIiwidGFyZ2V0X3VzZXIiOiJBIn0.rSWamyAYwuHCo7IFAgd1oRpSP7nzL7BF5t7ItqpKViM
在本项目中,我们规定每次请求时,需要在请求头中带上 token ,通过 token 检验权限,如没有,则说明当前为游客状态(或者是登陆 login 接口等)
下面是本次整合使用到的工具包
<!--JjWT-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!-- shiro -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<!--整合mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.1</version>
</dependency>
我们自定义一个jwtutil工具类实现token生成和解析
public class JWTUtil {
// 过期时间5分钟
private static final long EXPIRE_TIME = 5*60*1000;
//私钥
private static final String SECRETKEY = "imcode";
/**
* token解析
* @param token token
* @return io.jsonwebtoken.Claims
*/
public static Claims verify(String token){
Claims claims;
try {
claims = Jwts.parser()
.setSigningKey(DatatypeConverter.parseBase64Binary(SECRETKEY))
.parseClaimsJws(token)
.getBody();
}catch (ExpiredJwtException e){
throw new AuthenticationException("token 过期"+e.getMessage());
}catch (UnsupportedJwtException e){
throw new AuthenticationException("token 无效"+e.getMessage());
}catch (MalformedJwtException e){
throw new AuthenticationException("token 格式错误"+e.getMessage());
}catch (SignatureException e){
throw new AuthenticationException("token 签名无效"+e.getMessage());
}catch (IllegalArgumentException e){
throw new AuthenticationException("token 参数异常"+e.getMessage());
}catch (Exception e){
throw new AuthenticationException("token 令牌错误"+e.getMessage());
}
return claims;
}
/**
* 生成签名,5min后过期
* @param username 用户名
* @return 加密的token
*/
public static String sign(String username) {
JwtBuilder jwt = Jwts.builder();
//设置token唯一标识
jwt.setId(UUID.randomUUID().toString());
//设置主题
jwt.setSubject(username);
//设置签发者
jwt.setIssuer(SECRETKEY);
//设置签发日期
jwt.setIssuedAt(new Date());
//设置过期时间
jwt.setExpiration(new Date(System.currentTimeMillis() + EXPIRE_TIME));
//私钥
byte[] secretKeyBytes = DatatypeConverter.parseBase64Binary(SECRETKEY);
//签名
jwt.signWith(SignatureAlgorithm.HS256,secretKeyBytes);
return jwt.compact();
}
}
自定义一个token类替代shiro本身的userPasswordToken
public class JWTToken implements AuthenticationToken {
private String token;
public JWTToken(String token){
this.token = token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
然后自定义一个过滤器将拦截到的请求用我们自己的逻辑处理
@Slf4j
public class JWTFilter extends AccessControlFilter {
// 登录标识
private static String LOGIN_SIGN = "Authorization";
/**
* 是否允许通过,因为是无状态所以默认不通过,去自动登陆,返回false,调用onAccessDenied方法
* @param request
* @param response
* @param mappedValue
* @return boolean
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
return false;
}
@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
//使用token登陆
try {
HttpServletRequest req = (HttpServletRequest) servletRequest;
String authorization = req.getHeader(LOGIN_SIGN);
JWTToken token = new JWTToken(authorization);
// 提交给realm进行登入,如果错误他会抛出异常并被捕获
getSubject(servletRequest, servletResponse).login(token);
}catch (AuthenticationException e){
response401(e.getMessage(), servletResponse);
return false;
}
// 如果没有抛出异常则代表登入成功,返回true
return true;
}
/**
* 对跨域提供支持
*/
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
// 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
}
/**
* 将非法请求跳转到 /401
*/
private void response401(String erroInfo, ServletResponse resp) {
try {
HttpServletResponse response = (HttpServletResponse) resp;
response.setContentType("json/html;charset=utf8");
response.setCharacterEncoding("utf8");
PrintWriter pw = response.getWriter();
Msg msg = new Msg();
msg.setStatus("401");
msg.setMessage(erroInfo);
String result = JSON.toJSONString(msg);
pw.write(result);
} catch (IOException e) {
log.error(e.getMessage());
}
}
}
自定义一个realm处理自己的登陆认证以及授权逻辑
@Service
public class MyRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
@Autowired
private ResourceService resourceService;
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
Claims claims = (Claims) principalCollection.getPrimaryPrincipal();
String username = claims.getSubject();
if ("admin".equals(username)) {
Set<String> permissions = resourceService.list().stream().map(ResourceEntity::getPermission).collect(Collectors.toSet());
info.addStringPermissions(permissions);
info.addRole("admin");
return info;
}
//2.查询数据库用户
QueryWrapper<UserEntity> wrapper = new QueryWrapper<>();
wrapper.eq("username", username);
UserEntity user = userService.getOne(wrapper);
String userId = user.getUserId();
Set<String> permissions = userService.getPermissions(userId);
info.addStringPermissions(permissions);
return info;
}
//登陆认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) {
//1.获取用户输入的账户信息
String token = (String) authenticationToken.getPrincipal();
Claims claims = JWTUtil.verify(token);
return new SimpleAuthenticationInfo(claims, token, getName());
}
/**
* 找它的原因是这个方法返回true
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JWTToken;
}
}
配置shiro内容,因为使用jwt方式登陆是无状态的,所以配置中禁用了session会话
@Configuration
public class ShiroConfig {
//1.创建ShiroFilterFactory
@Bean("shiroFilter")
public ShiroFilterFactoryBean factory(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
// 添加自己的过滤器并且取名为jwt
Map<String, Filter> filterMap = new HashMap<>();
filterMap.put("jwt", new JWTFilter());
factoryBean.setFilters(filterMap);
factoryBean.setSecurityManager(securityManager);
factoryBean.setUnauthorizedUrl("/user/unauthorized");
/*
* 自定义url规则
* http://shiro.apache.org/web.html#urls-
*/
Map<String, String> filterRuleMap = new LinkedHashMap<>();
filterRuleMap.put("/user/login","anon");
filterRuleMap.put("/user/unauthorized", "anon");
filterRuleMap.put("/user/logout", "anon");
// 所有请求通过我们自己的JWT Filter
filterRuleMap.put("/**", "jwt");
// 访问401和404页面不通过我们的Filter
factoryBean.setFilterChainDefinitionMap(filterRuleMap);
return factoryBean;
}
@Bean
public CredentialMatcher credentialMatcher() {
//自定义的密码比较器
return new CredentialMatcher();
}
@Bean("securityManager")
public DefaultWebSecurityManager getManager(MyRealm realm) {
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
// realm.setCredentialsMatcher(credentialMatcher());
// 使用自己的realm
manager.setRealm(realm);
/*
* 关闭shiro自带的session,详情见文档
* http://shiro.apache.org/session-management.html#SessionManagement-StatelessApplications%28Sessionless%29
*/
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
manager.setSubjectDAO(subjectDAO);
return manager;
}
/**
* 下面的代码是添加注解支持
*/
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
// 强制使用cglib,防止重复代理和可能引起代理出错的问题
// https://zhuanlan.zhihu.com/p/29161098
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
}
到此shiro已经整合完成,再整合一下md5密码加密功能
public class MD5Util {
/**
* 密码md5加密
* @param password 原始密码
* @param salt 盐
* @return java.lang.String
*/
public static String createPasswd(String password,String salt){
return new Md5Hash(password,salt,2).toString();
}
}
@RestController
@RequestMapping(value = "/user")
@Api(
tags = {"系统用户管理"},
value = "系统用户管理",
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE
)
public class UserController {
@Autowired
private UserService userService;
@ApiOperation(value = "用户登陆")
@PostMapping("/login")
public Msg userLogin(@NotNull @RequestBody UserLoginDTO userLoginDTO) {
QueryWrapper<UserEntity> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("username",userLoginDTO.getUsername());
queryWrapper.eq("deleted",0);
UserEntity user = userService.getOne(queryWrapper);
if (null == user) throw new UnknownAccountException("用户名和密码错误");
if (user.getStatus() == 1) throw new UnknownAccountException("用户被禁用");
if (user.getStatus() == 2) throw new UnknownAccountException("用户被锁定");
//将传入密码加密与数据库密码对比
String password = MD5Util.createPasswd(userLoginDTO.getPassword(),user.getSalt());
if (!password.equals(user.getPassword())) {
throw new UnknownAccountException("用户名和密码错误");
}
//签发token返回
String token = JWTUtil.sign(userLoginDTO.getUsername());
Map<String, String> resultMap = new HashMap<>();
resultMap.put("Authorization", token);
return Msg.success(resultMap);
}
@Transactional
@RequiresRoles(value = "admin")
@PostMapping("/add")
@ApiOperation(value = "添加用户")
public Msg addUser(@RequestBody UserAddDTO userAddDTO){
//使用uuid随机生成6位的盐存入数据库
String uuId = UUID.randomUUID().toString();
String salt = uuId.substring(uuId.length()-6);
//加密密码
String password = MD5Util.createPasswd("2wsx@WSX",salt);
userAddDTO.setSalt(salt);
userAddDTO.setPassword(password);
String result = userService.addUser(userAddDTO);
return Msg.success(result);
}
//权限校验
@RequiresPermissions(logical = Logical.AND, value = {"userlist:view", "userlist:edit"})
@GetMapping(value = "/list")
public Msg<PageInfo<UserEntity>> getUserList(UserQueryDTO queryDTO) {
PageInfo<UserEntity> pageInfo = userService.getUserList(queryDTO);
return Msg.success(pageInfo);
}
@GetMapping("/unauthorized")
public Msg unauthorized() {
CodeMessage codeMessage = CodeMessage.UNAUTHORIZED;
Msg msg = new Msg();
msg.setStatus(codeMessage.getStatus());
msg.setMessage(codeMessage.getMessage());
return msg;
}
}
以上所有使用到的userService方法自己实现自己的逻辑,至此整合shiro与jjwt完成,仅供参考。