前置背景
- 为什么写下这篇文章?
因为需要实现一个设备管理系统的权限管理模块,在查阅很多博客以及其他网上资料之后,发现重复、无用的博客很多,因此写一篇文章来记录,以便后面复习。
-
涉及的知识点主要有下列知识点:
- JWT
- shiro
-
书写顺序
- 首先使用springboot 结合 jwt完成前后端分离的token认证。
- 其次结合shiro完成shiro+jwt的前后端分离的权限认证管理。
权限管理的表结构设计
- 一个user可以拥有多个role,一个role也可以被多个user拥有, 一个 角色拥有多个权限即功能,一个权限可以被多个role拥有。
用户、角色、权限类
表结构图
Part1: spring boot + jwt
这一部分就可以完成前后端分离项目的登录功能。在不需要添加权限管理的情况下,就可以满足需求。
Spring boot集成JWT
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.8.2</version>
</dependency>
复制代码
思路整理
- 为什么需要使用到jwt?
- jwt是什么
- Java如何使用jwt
在前后端分离的项目中,由服务器使用的会话管理
session
无法满足需求。需要一种技术做会话管理。因此选择
JWT
。
Json web token (JWT)
: 是目前流行的跨域认证解决方案,是一种基于 Token 的认证授权机制。
JWT
的数据结构分为三部分 header payload signature。 这三部分通过
.
连接,如下
Token示例:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.
eyJwYXNzd29yZCI6InN1cGVyIiwiZXhwIjoxNjYzMTIzNDgzLCJ1c2VybmFtZSI6InN1cGVyIn0.
5xVg6IuOLe_uVwwOaeyRDbTHRjfmIbNsnb-DP9-Ic20
复制代码
如何使用:当用户登录系统后,服务端给前端发送一个基于用户信息创建的token,然后在此后的每一次前端请求都会携带token。服务端通过拦截器拦截请求,同时验证携带的token是否正确。如果正确则放行请求,不正确则拒绝通过。 思路流程图:
token的创建和验证
JWTUtil.java
负责创建和验证jwt格式的token
public class JWTUtil {
private static final long EXPIRE_TIME = 3 * 60 * 1000;//默认3分钟
//私钥
private static final String TOKEN_SECRET = "privateKey";
public static String createToken(UserEntity userModel) {
try {
// 设置过期时间
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
log.info(String.valueOf(date));
// 私钥和加密算法
Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
// 设置头部信息
Map<String, Object> header = new HashMap<>(2);
header.put("Type", "Jwt");
header.put("alg", "HSA256");
// 返回token字符串
return JWT.create()
.withHeader(header)
.withClaim("username", userModel.getUsername())
.withExpiresAt(date)
.sign(algorithm);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 检验token是否正确
*
* @param **token**
* @return
*/
public static boolean verifyToken(String token, String username) {
log.info("验证token..");
try {
Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
JWTVerifier verifier = JWT.require(algorithm)
.withClaim("username",username).build();
// 验证不通过会抛出异常。
verifier.verify(token);
return true;
} catch (Exception e) {
log.info("verifyToken = {}",e.getMessage());
return false;
}
}
// 通过withClaim添加在token里面的数据都可以通过这种方式获取
public static String getUsername(String token){
DecodedJWT jwt = JWT.decode(token);
String username = String.valueOf(jwt.getClaim("username"));
if (StringUtils.hasLength(username)){
return username;
}
return null;
}
}
复制代码
拦截器的创建和配置
- 创建拦截器,拦截请求
@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 当前端是通过在请求里面以 token="xxxx.xxx.zzz"的方式传递时,通过getHeader("token")
// 的方式获取。
String token = request.getHeader("token");
log.info("token = {}",token);
if (token == null){
setReturnInfo((HttpServletResponse) response,401,"请携带token");
return false;
}
// 解析token中的数据,JWTUtil.getUsername();
// 在这里可以通过findUserByUsername的方式从数据源中获取数据
// 假定登录用户是super, 并传递给此方法传递参数
if ( !JWTUtil.verifyToken(token,"super")){
setReturnInfo((HttpServletResponse) response,401,"token已过期");
return false;
}
return true;
}
private static void setReturnInfo(HttpServletResponse httpResponse,int status,String msg) throws IOException {
log.info("token = null");
httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
httpResponse.setHeader("Access-Control-Allow-Origin", "*");
httpResponse.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=utf-8");
Map<String,String> result =new HashMap<>();
result.put("status",String.valueOf(status));
result.put("msg",msg);
httpResponse.getWriter().print(JSONUtils.toJSONString(result));
// 前端可根据返回的status判断
}
}
复制代码
2. 配置拦截器
InterceptorConfig.java
负责将使用了JwtUtil的拦截器配置进入Spring boot。
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Resource
private LoginInterceptor interceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
List<String> patterns = new ArrayList<>();
// 添加过滤路由 默认拦截所有请求
patterns.add("/**");
registry.addInterceptor(interceptor)
.addPathPatterns(patterns)
.excludePathPatterns("/user/login"); // 用户登录请求不拦截
}
}
复制代码
Part2:shiro+jwt
- 明确shiro是什么
- shiro的工作流程
- spring boot如何配置shiro框架
- shiro和jwt的整合
shiro是一个权限管理框架,相对于Spring Security而言,代码简单。适用场景多,不只局限于Java。 shiro的架构原理图:
Subject
:当前和系统交互的”用户”,可以是人,系统,第三方插件等。统称为Subject。
SecurityManager
:类似于Spring容器的一个容器,是Shiro的核心。管理众多的组件。
Authenticator
:认证组件,用户需要先通过系统认证,在进行用户授权,需要先判断系统中是否有这个用户,在进行后续操作,因此在这里就是进行系统认证的地方。
Authorizer
:授权,当一个用户是属于当前系统时,这个用户的一些操作就需要判断是否有权力去做这件事情。在这里就需要进行授权相关的
Realm
可以理解为数据源,就是在realm记录了那些属于本系统的用户,他们具有什么样的角色
就相当于在一个拥有多个公司的工业园区,人们需要有这个园区的卡片,才允许进入园区,而进入园区之后需要由本公司的门禁你才能进入公司,否则就不能进入公司一样。A公司的员工不能进入B公司。
Subject
就是员工,或者快递小哥,
SecurityManager
就是园区门禁系统,
Authenticator
就是门禁的闸机
Authorizer
的作用就是公司的门禁一样,
realm
就是园区系统的数据库,记录了系统的用户和权限信息 。
工作流程
:一个subject通过login()方法,将subject的信息提交给SecurityManager,SecurityManager调用自己的组件去判断,认证,授权等。shiro是通过filter来进行拦截请求的,因此在结合jwt时,就不需要interceptor就能达到预期的效果。
思路流程图:
第一步改造JwtToken
// AuthenticationToken 是shiro框架的。
public class JWTToken implements AuthenticationToken {
private String token;
public JWTToken(String token) {
this.token = token;
}
public String getToken() {
return token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
复制代码
JWTUtils代码不变,创建和验证逻辑如上。 第二步编写自己的数据源Realm
public class MyRealm extends AuthorizingRealm {
// 指定凭证匹配器。匹配器工作在认证后,授权前。
public MyRealm() {
this.setCredentialsMatcher(new JWTCredentialsMatcher());
}
@Resource
UserServiceInt userServiceInt;
// 判断token是否为JWTToken 必须重写
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JWTToken;
}
// 认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
log.info("AuthenticationInfo 开始认证");
String token = ((JWTToken) authenticationToken).getToken();
String username = JWT.decode(token).getClaim("username").asString();
// 从系统的数据库查找是否拥有这个用户,也可以提前把数据加载到Redis中,从redis中查找即可。
UserModel user = userServiceInt.getUserByUsername(username);
if (user == null) {
log.info("user为空");
// 认证不通过
return null;
}
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(
user,
token,
"myRealm"
);
return simpleAuthenticationInfo;
}
// 授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
log.info("开始授权...");
// 从PrincipalCollection获取user
UserModel userModel = (UserModel) principalCollection.getPrimaryPrincipal();
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
// 模拟数据库操作。实际上可以利用mybatis的级联查询,查询出用户的角色和权限信息。
if (userModel.getUsername().equals("super")){
// 添加用户角色
simpleAuthorizationInfo.addRole("admin");
// 添加用户权限
simpleAuthorizationInfo.addStringPermission("user:list");
}
return simpleAuthorizationInfo;
}
}
复制代码
public class JWTCredentialsMatcher implements CredentialsMatcher {
@Override
public boolean doCredentialsMatch(AuthenticationToken authenticationToken, AuthenticationInfo authenticationInfo) {
String token = ((JWTToken)authenticationToken).getToken();
log.info("JWTCredentialsMatcher token = {}",token);
UserModel userModel = (UserModel) authenticationInfo.getPrincipals().getPrimaryPrincipal();
log.info("JWTCredentialsMatcher token = {}",userModel.toString());
// 调用JwtUtils验证token即可
return JWTUtil.verifyToken(token, userModel.getUsername(), userModel.getPassword());
}
}
复制代码
第三:编写filter拦截前端请求
public class JwtFilter extends BasicHttpAuthenticationFilter {
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
log.info("isAccessAllowed : 验证是否拥有token");
String token = ((HttpServletRequest) request).getHeader("token");
HttpServletResponse servletResponse = (HttpServletResponse) response;
if (!StringUtils.hasLength(token)) {
try {
setReturnInfo(servletResponse, 401, "token为空");
} catch (IOException e) {
e.printStackTrace();
return false;
}
}
return executeLogin(request,response);
}
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) {
log.info("executeLogin : 执行登录");
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
String token = httpServletRequest.getHeader("token");
JWTToken jwtToken = new JWTToken(token);
// 提交给realm进行登入,如果错误他会抛出异常并被捕获
try {
getSubject(request, response).login(jwtToken);
} catch (Exception e) {
log.info("认证出现异常:{}", e.getMessage());
try {
setReturnInfo(httpServletResponse,401,"token错误");
} catch (IOException ex) {
ex.printStackTrace();
}
return false;
}
// 如果没有抛出异常则代表登入成功,返回true
return true;
}
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
log.info("登录失败");
return super.onAccessDenied(request, response);
}
/**
* 对跨域提供支持
*/
@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"));
httpServletResponse.setCharacterEncoding("UTF-8");
// 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(HttpStatus.OK.value());
return true;
}
return super.preHandle(request, response);
}
private static void setReturnInfo(HttpServletResponse response, int status, String msg) throws IOException {
response.setContentType("application/json;charset=utf-8");
Map<String, String> result = new HashMap<>();
result.put("status", String.valueOf(status));
result.put("msg", msg);
response.getWriter().write(JSONUtils.toJSONString(result));
}
复制代码
第四:配置shiro
@Configuration
public class ShiroConfig {
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
Map<String, Filter> filterMap = new HashMap<>();
filterMap.put("jwt",new JwtFilter());
shiroFilterFactoryBean.setFilters(filterMap);
Map<String,String> filterChainDefinitionMap = new LinkedHashMap<>();
filterChainDefinitionMap.put("/user/login","anon");
filterChainDefinitionMap.put("/**", "jwt");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
// 禁用session
@Bean
protected SessionStorageEvaluator sessionStorageEvaluator(){
DefaultWebSessionStorageEvaluator sessionStorageEvaluator = new DefaultWebSessionStorageEvaluator();
sessionStorageEvaluator.setSessionStorageEnabled(false);
return sessionStorageEvaluator;
}
@Bean("securityManager")
public SecurityManager securityManager(MyRealm userRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 指定SecurityManager的域
securityManager.setRealm(userRealm);
/*
* 关闭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);
securityManager.setSubjectDAO(subjectDAO);
return securityManager;
}
@Bean("myRealm")
public MyRealm shiroRealm() {
MyRealm shiroRealm = new MyRealm();
return shiroRealm;
}
@Bean
public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator autoProxyCreator = new DefaultAdvisorAutoProxyCreator();
autoProxyCreator.setProxyTargetClass(true);
return autoProxyCreator;
}
/**
* 开启shiro aop注解支持.
* 使用代理方式;所以需要开启代码支持;
* @param
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
}
复制代码
第五步在controller的方法上适用shiro权限控制的注解即可
@GetMapping("list")
// @RequiresAuthentication
@RequiresRoles(value = {"admin"})
// @RequiresPermissions(value = {"user:list"})
public List<UserModel> listUsers(){
return userServiceInt.listUser();
}