一、概述
JWT,即Json Web Token,JWT作为当下比较流行的身份认证方式之一主要的特点是无状态,把信息放在客户端,服务器端不需要保存session,适合在分布式环境下使用。
1、 传统的Token认证方式:用户登录,服务器端返回Token,并将Token保存在服务器端,以后用户再次访问时,需携带Token,服务器端获取Token后,再去数据库中获取Token进行校验。
2、 基于JWTToken认证方式:用户登录,服务器端给用户返回一个Token,但是服务器端不保存,以后用户再次访问时,需要携带Token,服务器端获取Token后,再做Token校验。
通过上面的对比,JWT跟传统方式最明显的区别就是服务端不保存Token,这样服务端校验Token时就少了很多数据库操作。
Jwttoken结构:JWT Token一般分为三个部分:Header、Payload、Signature。下图是一个JWT Token,每一种颜色对应一个部分。
Header:内部包含算法和类型。 生成的逻辑:将json转换成字符串,然后使用Base64Url进行编码加密 。
{
"alg": "HS256",
"typ": "JWT"
}
- Payload:主要存放自定义的信息,如userId、用户名等,但是不要把敏感信息放在这里。生成的逻辑:将json转换成字符串,然后使用Base64Url进行编码加密。
{
"userId": "1234567890",
"username": "John Doe",
"exp": 12345678950 超时时间
}
Signature:将第一部分、第二部分的密文使用 "." 拼接起来,然后使用HS256算法进行加密 + 加盐,HS256算法进行加密后的密文再进行Base64Url进行编码加密。
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
下面介绍一下jwt实现过程:
1、 第一步:用户提交用户名和密码给服务器端,如果登录成功,使用jwt创建一个token,并给用户返回;
2、 第二步:以后用户再来访问时,需携带token,后端需要对token进行校验;
3、 第三步:获取token,对token进行切割,对第二段进行Base64Url解密,获取Payload消息,检测token是否超时;
4、 第四步:将第一段和第二段进行拼接,然后使用HS256加密+加盐生成密文1,将第三段密文进行Base64Url解密生成密文2,最后比较两个密文1,2是否相等,如果相等表示token有效;
二、Shiro整合JWT
使用基于 Token 的身份验证方法,在服务端不需要存储用户的登录记录。
1、 客户端使用用户名跟密码请求登录;
2、 服务端收到请求,去验证用户名与密码;
3、 验证成功后,服务端会签发一个Token,再把这个Token发送给客户端;
4、 客户端收到Token以后可以把它存储起来,比如放在Cookie里;
5、 客户端每次向服务端请求资源的时候需要带着服务端签发的Token;
6、 服务端收到请求,然后去验证客户端请求里面带着的Token,如果验证成功,就向客户端返回请求的数据;
【a】引入JWT相关依赖
<!--jjwt 依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
【b】application.yml配置中增加JWT相关配置
#jwt 配置
jwt:
config:
加密密匙
secret: weixiaohuai
token有效期,单位秒
timeout: 3600
后端免认证接口 url
noAuthUrl: /userLogin,
【c】创建JWT工具类,负责生成JWT和解析JWT
import com.wsh.springboot.springbootshiro.properties.JWTProperties;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Date;
@Component
public class JwtUtils {
@Autowired
private JWTProperties jwtProperties;
/**
* 生成JWTToken
*
* @param id 用户id
* @param subject 用户名
* @return java.lang.String
*/
public String createJWTToken(String id, String subject) {
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
JwtBuilder builder = Jwts.builder()
.setId(id) //id
.setSubject(subject) //主题
.setIssuedAt(now) //签发时间
.signWith(SignatureAlgorithm.HS256, jwtProperties.getSecret()); //加密
//超时大于0 设置token超时
if (jwtProperties.getTimeout() > 0) {
//转换成超时毫秒
long timeout = nowMillis + (jwtProperties.getTimeout() * 1000);
builder.setExpiration(new Date(timeout));
}
return builder.compact();
}
/**
* 解析JWT
*/
public Claims parseJWToken(String jwtToken) {
return Jwts.parser()
.setSigningKey(jwtProperties.getSecret())
.parseClaimsJws(jwtToken)
.getBody();
}
}
【d】自定义JWT属性配置文件,用于注入配置文件中的值
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
/**
* 自定义JWT属性配置文件,用于注入配置文件中的值
*/
@Configuration
@Component
@ConfigurationProperties(prefix = "jwt.config")
public class JWTProperties {
/**
* jwt加密秘钥
*/
private String secret;
/**
* jwt有效时间
*/
private long timeout;
/**
* 过滤不需要认证的URL
*/
private String noAuthUrl;
public JWTProperties() {
}
public String getSecret() {
return secret;
}
public void setSecret(String secret) {
this.secret = secret;
}
public long getTimeout() {
return timeout;
}
public void setTimeout(long timeout) {
this.timeout = timeout;
}
public String getNoAuthUrl() {
return noAuthUrl;
}
public void setNoAuthUrl(String noAuthUrl) {
this.noAuthUrl = noAuthUrl;
}
}
【e】创建统一结果返回包装类
/**
* 自定义返回结果
*/
public class CustomResultSet {
private String code;
private String msg;
private String data;
public CustomResultSet() {
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public String getData() {
return data;
}
public void setData(String data) {
this.data = data;
}
public CustomResultSet(String code, String msg, String data) {
this.code = code;
this.msg = msg;
this.data = data;
}
}
【f】自定义过滤器CutomJWTFilter
我们使用的是 shiro 默认的权限拦截 Filter,而因为JWT的整合,我们需要自定义自己的过滤器 CutomJWTFilter,CutomJWTFilter 继承了 BasicHttpAuthenticationFilter,并部分方法进行了重写。
1、 检验请求头是否带有token((HttpServletRequest)request).getHeader("Token")!=null;
2、 如果带有token,执行shiro的login()方法,将token提交到Realm中进行检验;如果没有token,说明当前状态为游客状态(或者其他一些不需要进行认证的接口);
import com.fasterxml.jackson.databind.ObjectMapper;
import com.wsh.springboot.springbootshiro.entity.CustomResultSet;
import com.wsh.springboot.springbootshiro.entity.JWTToken;
import com.wsh.springboot.springbootshiro.properties.JWTProperties;
import org.apache.shiro.authz.UnauthorizedException;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.bind.annotation.RequestMethod;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @Description 自定义JWT过滤器, 继承自BasicHttpAuthenticationFilter
* @Date 2022/11/7 20:23
* @Author weishihuai
* 说明:
*/
public class CustomJWTFilter extends BasicHttpAuthenticationFilter {
private static final Logger logger = LoggerFactory.getLogger(CustomJWTFilter.class);
//请求头中"access_token"
private static final String ACCESS_TOKEN = "access_token";
//JWT属性配置信息
private JWTProperties jwtProperties;
private AntPathMatcher pathMatcher = new AntPathMatcher();
//因为CustomJWTFilter并没有注册到IOC容器中,所以不能使用@Autowired注入JWTProperties,得使用setter或者构造方法注入
public CustomJWTFilter(JWTProperties jwtProperties) {
this.jwtProperties = jwtProperties;
}
/**
*
* 为什么最终返回的都是true,即允许访问?
* 例如我们提供一个地址 GET /list, 登入用户和游客看到的内容是不同的, 如果在这里返回了false,请求会被直接拦截,用户看不到任何东西
* 所以我们在这里返回true,Controller中可以通过 subject.isAuthenticated() 来判断用户是否登入.
* 如果有些资源只有登入用户才能访问,我们只需要在方法上面加上 @RequiresAuthentication 注解即可.
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws UnauthorizedException {
logger.info("CustomJWTFilter ---> isAccessAllowed().....");
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String[] split = jwtProperties.getNoAuthUrl().split(",");
for (String url : split) {
//如果是在配置文件中配置的免认证URL,则直接返回true,表示放行
if (pathMatcher.match(url, httpServletRequest.getRequestURI())) {
return true;
}
}
//判断请求的请求头是否带上access_token属性
if (isLoginAttempt(request, response)) {
//如果请求头中包含access_token属性,则执行executeLogin方法进行登入操作,检查access_token是否正确
try {
return executeLogin(request, response);
} catch (IOException e) {
e.printStackTrace();
}
} else {
try {
this.returnErrorMsg(response, "token为空");
} catch (IOException e) {
e.printStackTrace();
}
}
return false;
}
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws IOException {
logger.info("CustomJWTFilter ---> executeLogin().....");
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String token = httpServletRequest.getHeader(ACCESS_TOKEN);
try {
// 提交给realm进行登入,如果错误他会抛出异常并被捕获
getSubject(request, response).login(new JWTToken(token));
// 如果没有抛出异常则代表登入成功,返回true
return true;
} catch (Exception e) {
e.printStackTrace();
this.returnErrorMsg(response, "executeLogin--->token认证失败");
return false;
}
}
/**
* 判断用户是否想要登入。
* 检查请求头中是否包含access_token即可
*/
@Override
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
logger.info("CustomJWTFilter ---> isLoginAttempt().....");
HttpServletRequest req = (HttpServletRequest) request;
return null != req.getHeader(ACCESS_TOKEN);
}
/**
* 返回自定义错误信息
*
* @param response
* @param msg
* @throws IOException
*/
private void returnErrorMsg(ServletResponse response, String msg) throws IOException {
CustomResultSet resultSet = new CustomResultSet("500", msg, null);
//响应token为空
response.setContentType("application/json;charset=UTF-8");
response.setCharacterEncoding("UTF-8");
//清空第一次流响应的内容
response.resetBuffer();
//转成json格式
ObjectMapper object = new ObjectMapper();
String asString = object.writeValueAsString(resultSet);
response.getWriter().println(asString);
}
/**
* 对跨域提供支持
*/
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
logger.info("CustomJWTFilter ---> preHandle().....");
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);
}
所有的请求都会先经过Filter,所以我们继承官方的BasicHttpAuthenticationFilter,并且重写鉴权的方法。代码的执行流程preHandle -> isAccessAllowed -> isLoginAttempt -> executeLogin。
【g】自定义JWTToken,继承自AuthenticationToken
JWTToken差不多就是Shiro用户名密码的载体。因为我们是前后端分离,服务器无需保存用户状态,所以不需要RememberMe这类功能,我们简单的实现下AuthenticationToken接口即可。
/**
* 自定义JWTToken,继承AuthenticationToken
*/
public class JWTToken implements AuthenticationToken {
private String token;
public JWTToken(String token) {
this.token = token;
}
public JWTToken() {
}
@Override
public Object getPrincipal() {
return this.token;
}
@Override
public Object getCredentials() {
return this.token;
}
}
【h】Shiro全局配置类中增加自定义jwtFilter过滤器,用来拦截并处理携带JWT token的请求
package com.wsh.springboot.springbootshiro.config;
import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;
import com.wsh.springboot.springbootshiro.filter.CustomJWTFilter;
import com.wsh.springboot.springbootshiro.listener.CustomShiroSessionListener;
import com.wsh.springboot.springbootshiro.properties.JWTProperties;
import com.wsh.springboot.springbootshiro.realm.MyShiroRealm;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.session.SessionListener;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.session.mgt.eis.SessionDAO;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.filter.DelegatingFilterProxy;
import javax.servlet.Filter;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* @Description: Shiro全局配置类
* @author DDKK.COM 弟弟快看,程序员编程资料站
* @Date: 2022/11/3 09:23
* <p>
* 三大组件:
* 1. Subject: 用户主体(把操作交给SecurityManager)
* 2. SecurityManager:安全管理器(关联Realm)
* 3. Realm:Shiro连接数据的桥梁
*/
@Configuration
public class ShiroConfiguration {
@Autowired
private JWTProperties jwtProperties;
/**
* 将Realm注册到securityManager中
*
* @return
*/
@Bean("securityManager")
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
defaultWebSecurityManager.setRealm(myShiroRealm());
//将cookie管理器交给SecurityManager进行管理
defaultWebSecurityManager.setRememberMeManager(rememberMeManager());
//设置Redis缓存管理器
defaultWebSecurityManager.setCacheManager(redisCacheManager());
/*
* 关闭shiro自带的session
*/
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
defaultWebSecurityManager.setSubjectDAO(subjectDAO);
return defaultWebSecurityManager;
}
/**
* 配置自定义的Realm
*
* @param matcher
* @return
*/
@Bean
public MyShiroRealm myShiroRealm() {
MyShiroRealm myShiroRealm = new MyShiroRealm();
//配置密码加密
return myShiroRealm;
}
/**
* 如果没有此name,将会找不到shiroFilter的Bean
* <p>
* Shiro内置过滤器,可以实现权限相关的拦截器
* 常用的过滤器:
* anon: 无需认证(登录)可以访问
* authc: 必须认证才可以访问
* user: 如果使用rememberMe的功能可以直接访问
* perms: 该资源必须得到资源权限才可以访问
* role: 该资源必须得到角色权限才可以访问
*/
@Bean
public ShiroFilterFactoryBean shiroFilter(@Qualifier("securityManager") DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
LinkedHashMap<String, Filter> filtersMap = new LinkedHashMap<>();
//添加自己的自定义jwt过滤器,并取名为jwt
filtersMap.put("jwt", new CustomJWTFilter(jwtProperties));
shiroFilterFactoryBean.setFilters(filtersMap);
//表示指定登录页面
shiroFilterFactoryBean.setLoginUrl("/userLogin");
//登录成功后要跳转的链接
shiroFilterFactoryBean.setSuccessUrl("/success");
//未授权页面
shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized");
//拦截器, 配置不会被拦截的链接 顺序判断
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
//所有匿名用户均可访问到Controller层的该方法下
filterChainDefinitionMap.put("/forceLogout", "anon");
filterChainDefinitionMap.put("/index", "anon");
filterChainDefinitionMap.put("/userLogin", "anon");
filterChainDefinitionMap.put("/admin", "roles[admin]");
filterChainDefinitionMap.put("/user", "roles[user]");
//user表示配置记住我或认证通过可以访问的地址
filterChainDefinitionMap.put("/remember", "user");
filterChainDefinitionMap.put("/logout", "logout");
//authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问
// filterChainDefinitionMap.put("/**", "authc");
//所有url都必须认证通过jwt过滤器才可以访问
filterChainDefinitionMap.put("/**", "jwt");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
/**
* SpringShiroFilter首先注册到spring容器
* 然后被包装成FilterRegistrationBean
* 最后通过FilterRegistrationBean注册到servlet容器
*
* @return
*/
@Bean
public FilterRegistrationBean delegatingFilterProxy() {
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
DelegatingFilterProxy proxy = new DelegatingFilterProxy();
proxy.setTargetFilterLifecycle(true);
proxy.setTargetBeanName("shiroFilter");
filterRegistrationBean.setFilter(proxy);
return filterRegistrationBean;
}
/**
* 开启Shiro注解配置
*
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
/**
* 配置Redis缓存管理器
*/
@Bean
public RedisCacheManager redisCacheManager() {
RedisCacheManager redisCacheManager = new RedisCacheManager();
//设置redis管理器
redisCacheManager.setRedisManager(redisManager());
return redisCacheManager;
}
// /**
// * Ehcache缓存管理器
// * @return
// */
// @Bean
// @Primary
// public EhCacheManager ehCacheManager() {
// EhCacheManager ehCacheManager = new EhCacheManager();
// ehCacheManager.setCacheManagerConfigFile("classpath:config/shiro-ehcache-config.xml");
// return ehCacheManager;
// }
/**
* 为了在thymeleaf中使用shiro的自定义tag
*/
@Bean
public ShiroDialect shiroDialect() {
return new ShiroDialect();
}
/**
* 配置redis管理器
*/
@Bean
public RedisManager redisManager() {
RedisManager redisManager = new RedisManager();
//设置一小时超时,单位是秒
redisManager.setExpire(3600);
return redisManager;
}
}
【i】关闭shiro自带的session
因为用了jwt的访问认证,所以要把默认session支持关掉。即不保存用户登录状态,保证每次请求都重新认证。
/*
* 关闭shiro自带的session
*/
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
defaultWebSecurityManager.setSubjectDAO(subjectDAO);
【j】配置自己实现的ShiroRealm
import com.wsh.springboot.springbootshiro.entity.JWTToken;
import com.wsh.springboot.springbootshiro.entity.Permission;
import com.wsh.springboot.springbootshiro.entity.Role;
import com.wsh.springboot.springbootshiro.entity.User;
import com.wsh.springboot.springbootshiro.mapper.PermissionMapper;
import com.wsh.springboot.springbootshiro.mapper.RoleMapper;
import com.wsh.springboot.springbootshiro.mapper.UserMapper;
import com.wsh.springboot.springbootshiro.util.JwtUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class MyShiroRealm extends AuthorizingRealm {
private static final Logger logger = LoggerFactory.getLogger(MyShiroRealm.class);
@Autowired
private UserMapper userMapper;
@Autowired
private RoleMapper roleMapper;
@Autowired
private PermissionMapper permissionMapper;
@Autowired
private JwtUtils jwtUtils;
/**
* 支持自定义认证令牌
* 必须重写此方法,不然Shiro会报错
* 限定这个 Realm 只处理我们自定义的 JwtToken
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JWTToken;
}
/**
* 默认使用此方法进行用户名正确与否验证,错误抛出异常即可
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
logger.info("MyShiroRealm ---> doGetAuthenticationInfo()认证.....");
// 这里的 token是从 CutomJWTFilter 的 executeLogin 方法传递过来的
String token = (String) authenticationToken.getCredentials();
String username;
try {
username = jwtUtils.parseJWToken(token).getSubject();
} catch (Exception e) {
//抛出token认证失败
throw new AuthenticationException("doGetAuthenticationInfo--->token认证失败");
}
// 通过用户名到数据库查询用户信息
User user = userMapper.findUserByName(username);
if (user == null) {
throw new UnknownAccountException("用户不存在!");
}
return new SimpleAuthenticationInfo(user.getUsername(), token, getName());
}
/**
* 授权相关方法
*
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
logger.info("MyShiroRealm ---> doGetAuthorizationInfo()授权.....");
//1.获取用户名
String username = (String) principalCollection.getPrimaryPrincipal();
logger.info("username:" + username);
//返回AuthorizationInfo授权类的子类
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
//2.根据用户名查询用户所有的角色信息
List<Role> allRoleList = roleMapper.getAllRoleListByUsername(username);
Set<String> rolesSet = new HashSet<>();
for (Role r : allRoleList) {
String roleName = r.getName();
rolesSet.add(roleName);
}
logger.info("用户:{} 拥有的角色有:{}", username, rolesSet);
//设置用户角色信息
simpleAuthorizationInfo.setRoles(rolesSet);
//3.根据用户名查询用户所有的权限信息
List<Permission> allPermissionList = permissionMapper.getAllPermissionListByUsername(username);
Set<String> permissionSet = new HashSet<>();
for (Permission permission : allPermissionList) {
String permissionName = permission.getName();
permissionSet.add(permissionName);
}
simpleAuthorizationInfo.setStringPermissions(permissionSet);
logger.info("用户:{} 拥有的权限有:{}", username, permissionSet);
return simpleAuthorizationInfo;
}
}
【k】修改登录接口,登录成功发放jwt token
@RequestMapping(value = "/userLogin", method = RequestMethod.POST)
@ResponseBody
public CustomResultSet toLogin(String username, String password) {
// 密码加密
String md5Password = new SimpleHash("MD5", password, username, 1024).toString();
User user = userMapper.findUserByName(username);
if (user != null && md5Password.equals(user.getPassword())) {
String jwtToken = jwtUtils.createJWTToken(user.getId(), user.getUsername());
//这里只是简单的返回到前台,实际项目中这里可以将签发的JWT token设置到 HttpServletResponse 的Header中
//((HttpServletResponse) response).setHeader(JwtUtils.AUTH_HEADER, jwtToken);
return new CustomResultSet("200", "获取token成功", jwtToken);
}
return new CustomResultSet("500", "用户不存在或者密码错误", null);
}
【l】新建一个测试jwt的controller方法
@RequestMapping("/jwtTest")
@ResponseBody
@RequiresPermissions("admin:list")
public String jwtTest() {
return "测试shiro整合jwt......";
}
【m】测试启动项目,使用admin/123456进行登录,登录之后,可以看到返回的结果信息中包含有access_token。
{"code":"200","msg":"获取token成功","data":"eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxIiwic3ViIjoiYWRtaW4iLCJpYXQiOjE2MDQ4MzkwMzAsImV4cCI6MTYwNDg0MjYzMH0.Ld4T92poGelbnS6GTWSCY2JGvFVlQWCsy1NPgOnfln8"}
这里我们我们选用postman进行请求测试,分为如下三种情况进行测试:
- (1)、不带access_token
可以看到,当我们请求中没有携带token,后台返回的错误信息如下:
{
"code": "500",
"msg": "token为空",
"data": null
}
- (2)、带上错误的access_token
可见,当我们携带的token是无效时,后台返回的错误信息如下:
{
"code": "500",
"msg": "executeLogin--->token认证失败",
"data": null
}
- (3)、带上正确的access_token
可见,当我们携带登录接口返回的正确的token是,请求正常返回数据。
至此,我们简单实现了Shiro整合JWT实现认证功能。
三、总结
本文主要总结了Shiro如何整合JWT来实现用户认证功能,当然仅仅是简单实现,还有很多需要优化的地方:
- 暂时还没实现动态刷新access_token功能;
- access_token缓存问题;
- ...
由于笔者水平有限,文中难免存在没有考虑到的地方,还望大家指点,相互学习,一起进步!