shiro+JWT做一个简单的权限管理功能
2020-08-17 本文已影响0人
自学java的菜鸟小赵
之前在网上查找一些关于shiro整合jwt的案例,都比较繁琐,不适合新手学习,自己把网上的案例学习一遍,做了一个类似,也更加方便了解的权限管理功能。
数据库设计,网上一些案例都是设计好几个表关联,不容易理解,这里我就设计了一个表,,如果用户角色是admin,后面会设计只有admin角色才能访问的方法,pression设计vip权限和normal权限,后面也会设计带有vip权限或者normal权限才能访问的方法。
user实验步骤
1.导入依赖
<dependency>
<!-- shiro-->
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.3.2</version>
</dependency>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>
2.相关类的设计
shiroconfig,shirio的相关配置文件,都是固定的模板,我们主要学习ShiroFilterFactoryBean 处理拦截资源文件问题,在里面我们添加自己的过滤器,所有的请求都会经过自定义的过滤器进行过滤
@Configuration
public class ShiroConfig {
@Bean("securityManager")
public DefaultWebSecurityManager getManager(MyRealm realm) {
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
// 使用自己的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("shiroFilter")
public ShiroFilterFactoryBean factory(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
// 添加自己的过滤器并且取名为jwt
Map<String, Filter> filterMap = new HashMap<>(4);
filterMap.put("jwt", new JWTFilter());
factoryBean.setFilters(filterMap);
factoryBean.setSecurityManager(securityManager);
factoryBean.setUnauthorizedUrl("/401");
/**
* 自定义url规则
*/
Map<String, String> filterRuleMap = new HashMap<>(4);
// 所有请求通过我们自己的JWT Filter
filterRuleMap.put("/**", "jwt");
// 访问401和404页面不通过我们的Filter
filterRuleMap.put("/401", "anon");
factoryBean.setFilterChainDefinitionMap(filterRuleMap);
return factoryBean;
}
/**
* 下面的代码是添加注解支持
*/
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
// 强制使用cglib,防止重复代理和可能引起代理出错的问题
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;
}
}
JWTFilter,自定义的过滤器,当发送过来的请求有携带token信息,就会执行executeLogin方法,进入到自定义的Realm进行授权和认证的功能,因为是引入他人的模板,所以有些地方不需要可以注释掉,对跨域的支持暂时用不到。这里我在每一个方法后面都添加一个输出语句是为了方便对程序运行过程的理解。
public class JWTFilter extends BasicHttpAuthenticationFilter {
private Logger LOGGER = LoggerFactory.getLogger(this.getClass());
/**
* 判断用户是否想要登入。
* true:是要登录
*
*/
@Override
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
System.out.println("*****进入isLoginAttempt");
HttpServletRequest req = (HttpServletRequest) request;
String authorization = req.getHeader("token");
return authorization != null;
}
/**
*
*/
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
//得到token信息
String authorization = httpServletRequest.getHeader("token");
JWTToken token = new JWTToken(authorization);
// 提交给realm进行登入,如果错误他会抛出异常并被捕获
getSubject(request, response).login(token);
// 如果没有抛出异常则代表登入成功,返回true
return true;
}
/**
* 这里我们详细说明下为什么最终返回的都是true,即允许访问
* 例如我们提供一个地址 GET /article
* 登入用户和游客看到的内容是不同的
* 如果在这里返回了false,请求会被直接拦截,用户看不到任何东西
* 所以我们在这里返回true,Controller中可以通过 subject.isAuthenticated() 来判断用户是否登入
* 如果有些资源只有登入用户才能访问,我们只需要在方法上面加上 @RequiresAuthentication 注解即可
* 但是这样做有一个缺点,就是不能够对GET,POST等请求进行分别过滤鉴权(因为我们重写了官方的方法),但实际上对应用影响不大
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
if (isLoginAttempt(request, response)) {
try {
System.out.println("执行executeLogin");
executeLogin(request, response);
} catch (Exception e) {
response401(request, response);
}
}
System.out.println("没有执行executeLogin");
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(ServletRequest req, ServletResponse resp) {
try {
HttpServletResponse httpServletResponse = (HttpServletResponse) resp;
httpServletResponse.sendRedirect("/401");
} catch (IOException e) {
LOGGER.error(e.getMessage());
}
}
}
当发送过来的请求携带token信息,就会进入到自定义Realm类中进行认证,继承AuthorizingRealm 类并重写里面的认证和授权的方法。首先观察doGetAuthenticationInfo方法,这里我们首先获得token信息,然后通过JwtUtils工具类对token信息进行解析,获得当前用户的姓名,然后去数据库中查到当前用户,如果当前用户存在,则把当前用户信息保存到 SimpleAuthenticationInfo对象中,后面做授权要用到(第一个参数用来保存当前用户信息,也可以保存token信息,不唯一,自己定义)
@Configuration
public class MyRealm extends AuthorizingRealm {
@Autowired
private UserService userServicel;
/*
必须要加,不然程序会报错
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JWTToken;
}
/*
这里的PrincipalCollection对应SimpleAuthenticationInfo的第一个参数
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) {
System.out.println("执行————————》AuthorizationInfo");
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
// User user=(User)principal.getPrimaryPrincipal();
// System.out.println(user);
String username = JwtUtils.getUsername(principal.toString());
System.out.println(username);
User user = userServicel.getOne(new QueryWrapper<User>().eq("username", username));
info.addRole(user.getRole());
info.addStringPermission(user.getPression());
return info;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
System.out.println("执行————————》AuthenticationInfo");
String token = (String) auth.getCredentials();
System.out.println("token信息"+token);
// 解密获得username,用于和数据库进行对比
String username = JwtUtils.getUsername(token);
System.out.println(username);
if (username == null) {
throw new AuthenticationException("token invalid");
}
User user = userServicel.getOne(new QueryWrapper<User>().eq("username", username));
if (user == null) {
throw new AuthenticationException("User didn't existed!");
}
return new SimpleAuthenticationInfo(token, token, "my_realm");
}
}
这里我们可以对认证功能进行试验。使用postman测试,创建一个loginController类,得到当前用户的token信息。
@PostMapping("/login")
public R login(@RequestBody JSONObject requestJson){
System.out.println(requestJson);
String username = requestJson.getString("username");
String password = requestJson.getString("password");
String token = JwtUtils.getJwtToken(username, password);
return R.ok().data("token",token);
}
image.png
我们随便写一个请求,用户测试
@GetMapping("user")
//@RequiresRoles("admin")
//@RequiresRoles(logical = Logical.OR, value = {"user", "admin"})
public R getUser(){
return R.ok();
}
没有携带token信息
image.png查看输出台
image.png携带token信息
image.png查看输出台,执行了AuthenticationInfo方法
image.png现在我们就可以开始做授权认证的功能,观察doGetAuthorizationInfo方法,这里的PrincipalCollection对应SimpleAuthenticationInfo的第一个参数,通过principal获取当前用户,并授予权限,使用addRole添加用户角色,使用 addStringPermission添加用户的权限,还需要了解2个注解,@RequiresRoles 授权方法中给用户添加角色,@RequiresPermissions 授权方法中给用户添加权限。
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) {
System.out.println("执行————————》AuthorizationInfo");
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
// User user=(User)principal.getPrimaryPrincipal();
// System.out.println(user);
String username = JwtUtils.getUsername(principal.toString());
System.out.println(username);
User user = userServicel.getOne(new QueryWrapper<User>().eq("username", username));
info.addRole(user.getRole());
info.addStringPermission(user.getPression());
return info;
}
我们在之前的请求方法中添加@RequiresRoles注解,可以从控制台中看到执行了授权的方法,但是因为当前用户的角色是guest,所以会报错。
@GetMapping("user")
@RequiresRoles("admin")
//@RequiresRoles(logical = Logical.OR, value = {"user", "admin"})
public R getUser(){
return R.ok();
}
image.png
image.png
修改用户的角色为guest,再发送请求
@GetMapping("user")
@RequiresRoles("guest")
//@RequiresRoles(logical = Logical.OR, value = {"user", "admin"})
public R getUser(){
return R.ok();
}