本文章参考两位大佬写的博客完成的
- 一看就懂!Springboot +Shiro +VUE 前后端分离式权限管理系统_大誌的博客-CSDN博客_shiro vue
- SpringBoot+Shiro+JWT实现权限管理_西瓜不甜柠檬不酸的博客-CSDN博客_springboot+shiro+jwt
学习shiro之前,建议先学习springsecurity,将两个框架进行对比的方式学习,会有更加深的印象。我之前写过一篇关于springsecurity的博客,你可以参考参考,个人感觉挺详细的。超全的springboot+springsecurity前后端分离简单实现!!!_码上编程的博客-CSDN博客。
本篇文章是使用shiro做认证授权的,基于token的前后端分离的小demo,而token我把它放在redis缓存里面,极大程度地模拟实战效果,本篇文章每一步都有详细步骤,看不懂可以看多几遍!!! 源代码在此文章最底部!!!
未登录情况下访问目标资源, 将会提示需要登录的字样。 /hello和/index是我在controller实现的简单接口。
账号错误,并返回登录失败的字样
密码错误,并返回登录失败的字样
登录并返回登录成功字样以及token
登录成功的情况下访问拥有指定权限的目标资源,并返回该目标资源
登录成功的情况下访问不具有指定权限的目标资源,并返回权限不足的提示字样
注销,并返回注销成功字样,同时删除缓存里面的token值
springboot、shiro、mybatis-plus、mysql、redis、gson、lombok
简单理解: shiro最重要的三个部分:
复杂理解就在流程图里面: 强烈建议搞清楚流程图!!!!!
- 不走doGetAuthenticationInfo(AuthenticationToken token) 方法, 通过在pom文件添加这样的依赖即可解决此问题!
org.springframework.boot spring-boot-starter-aop - 拦截所有路径位置要放正确,要不然它会拦截所有路径,即 filterMap.put("/**","auth"); 要放在最后面
filterMap.put("/login","anon"); //放行login接口
filterMap.put("/logout","anon"); //放行logout接口
filterMap.put("/**","auth"); //拦截所有路径
pom文件
org.springframework.boot
spring-boot-starter-aop
org.springframework.boot
spring-boot-starter-data-redis
com.google.code.gson
gson
2.8.2
org.springframework.boot
spring-boot-starter-web
mysql
mysql-connector-java
runtime
com.auth0
java-jwt
3.2.0
org.projectlombok
lombok
true
com.baomidou
mybatis-plus-boot-starter
3.4.1
org.apache.velocity
velocity-engine-core
2.2
com.baomidou
mybatis-plus-generator
3.4.1
test
org.apache.shiro
shiro-spring
1.3.2
org.springframework.boot
spring-boot-starter-test
test
org.junit.vintage
junit-vintage-engine
数据库设计 ,这里我偷懒了,用的还是springsecurity那个例子的数据库
User.java
Data
@EqualsAndHashCode(callSuper = false)
public class User implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
private String account;
private String password;
private String role;
}
UserMapper.java继承BaseMapper
@Repository
public interface UserMapper extends BaseMapper {
}
UserService.java
public interface UserService extends IService {
//根据账号查找用户
User findByUsername(String username);
}
UserServiceImpl.java
@Service
public class UserServiceImpl extends ServiceImpl implements UserService {
@Autowired
UserMapper userMapper;
@Override
public User findByUsername(String username) {
//相当于select * from user where account='${username}'
QueryWrapper wrapper=new QueryWrapper<>();
wrapper.eq("account",username);
//user即为查询结果
return userMapper.selectOne(wrapper);
}
}
Msg.java,封装了返回的结果集,这个看个人的,怎么开心怎么来!
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Msg {
int code; //错误码
String Message; //消息提示
Map data=new HashMap(); //数据
//无权访问
public static Msg denyAccess(String message){
Msg result=new Msg();
result.setCode(300);
result.setMessage(message);
return result;
}
//操作成功
public static Msg success(String message){
Msg result=new Msg();
result.setCode(200);
result.setMessage(message);
return result;
}
//客户端操作失败
public static Msg fail(String message){
Msg result=new Msg();
result.setCode(400);
result.setMessage(message);
return result;
}
public Msg add(String key,Object value){
this.data.put(key,value);
return this;
}
}
ShiroConfig.java,详细请看注释
@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean=new ShiroFilterFactoryBean();
//关联 DefaultWebSecurityManager
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
//添加shiro内置的过滤器
/*
* anon: 无需认证就可以访问
* authc: 必须认证才能访问
* user: 必须拥有记住我功能才能用
* perms: 拥有对某个资源的权限才能访问
* role: 拥有某个角色权限才能访问
* */
//添加过滤器
Map filters=new HashMap<>();
filters.put("auth",new AuthFilter()); //自定义的认证授权过滤器
shiroFilterFactoryBean.setFilters(filters); //添加自定义的认证授权过滤器
//要拦截的路径放在map里面
Map filterMap=new LinkedHashMap();
filterMap.put("/login","anon"); //放行login接口
filterMap.put("/logout","anon"); //放行logout接口
filterMap.put("/**","auth"); //拦截所有路径, 它自动会跑到 AuthFilter这个自定义的过滤器里面
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
return shiroFilterFactoryBean;
}
//配置securityManager的实现类,变向的配置了securityManager
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager(AuthRealm authRealm){
DefaultWebSecurityManager defaultWebSecurityManager=new DefaultWebSecurityManager();
//关联realm
defaultWebSecurityManager.setRealm(authRealm);
/*
* 关闭shiro自带的session
*/
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
defaultWebSecurityManager.setSubjectDAO(subjectDAO);
return defaultWebSecurityManager;
}
//将自定义realm注入到 DefaultWebSecurityManager
@Bean
public AuthRealm authRealm(){
return new AuthRealm();
}
//通过调用Initializable.init()和Destroyable.destroy()方法,从而去管理shiro bean生命周期
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
//开启shiro权限注解
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager defaultWebSecurityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(defaultWebSecurityManager);
return advisor;
}
}
自定义认证授权过滤器 AuthFilter , 正常来说对于常用部分需要封装,这样代码冗余度就不会那么大,也比较优雅,但是这只是个小demo,不想把它弄的那么复杂、不利于理解,索性就不封装了。
1、用户发起请求首先进入isAccessAllowed()方法,拦截除了option请求 以外的请求, 浏览器机制就是:在发post、get请求之前,首先会发个option请求进行试探,所以要放行option请求通过,否则你的get、post请求永远进不来。
2、token不为空的情况下则生成属于自己的token,即创建一个类使其继承 UsernamePasswordToken此类
3、然后进入onAccessDenied()方法去校验token是否存在,并执行executeLogin(request,response)方法
public class AuthFilter extends AuthenticatingFilter {
Gson gson=new Gson();
//生成自定义token
@Override
protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest= (HttpServletRequest) request;
//从header中获取token
String token=httpServletRequest.getHeader("token");
return new AuthToken(token);
}
//所有请求全部拒绝访问
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
//允许option请求通过
if (((HttpServletRequest) request).getMethod().equals(RequestMethod.OPTIONS.name())) {
return true;
}
return false;
}
//拒绝访问的请求,onAccessDenied方法先获取 token,再调用executeLogin方法
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest= (HttpServletRequest) request;
HttpServletResponse httpServletResponse= (HttpServletResponse) response;
String token=httpServletRequest.getHeader("token"); //获取请求token
//StringUtils.isBlank(String str) 判断str字符串是否为空或者长度是否为0
if(org.apache.commons.lang3.StringUtils.isBlank(token)){
httpServletResponse.setHeader("Access-Control-Allow-Credentials", "true");
httpServletResponse.setHeader("Access-Control-Allow-Origin",httpServletRequest.getHeader("Origin") );
httpServletResponse.setCharacterEncoding("UTF-8");
Msg msg= Msg.fail("请先登录");
httpServletResponse.getWriter().write(gson.toJson(msg));
return false;
}
return executeLogin(request,response);
}
//token失效时调用
@Override
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
HttpServletRequest httpServletRequest= (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.setContentType("application/json;charset=utf-8");
httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
httpResponse.setHeader("Access-Control-Allow-Origin", httpServletRequest.getHeader("Origin"));
httpResponse.setCharacterEncoding("UTF-8");
try {
//处理登录失败的异常
Throwable throwable = e.getCause() == null ? e : e.getCause();
Msg msg=Msg.fail("登录凭证已失效,请重新登录");
httpResponse.getWriter().write(gson.toJson(msg));
} catch (IOException e1) {
}
return false;
}
}
自定义 AuthToken.java 去继承 UsernamePasswordToken这个类,因为源码里的subject.login(token)需要传入一个自定义的token参数
public class AuthToken extends UsernamePasswordToken{
String token;
public AuthToken(String token){
this.token=token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
AuthRealm.java是自定义的realm,继承AuthorizingRealm,实现doGetAuthenticationInfo(AuthenticationToken token)认证和 doGetAuthorizationInfo(PrincipalCollection principals)授权
public class AuthRealm extends AuthorizingRealm {
@Autowired
UserServiceImpl userServiceImpl;
@Autowired
StringRedisTemplate stringRedisTemplate;
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//获取前端传来的token
String accessToken= (String) token.getPrincipal();
//redis缓存中这样存值, key为token,value为username
//根据token去缓存里查找用户名
String username=stringRedisTemplate.opsForValue().get(accessToken);
if(username==null){
//查找的用户名为空,即为token失效
throw new IncorrectCredentialsException("token失效,请重新登录");
}
User user = userServiceImpl.findByUsername(username);
if(user==null){
throw new UnknownAccountException("用户不存在!");
}
//此方法需要返回一个AuthenticationInfo类型的数据
// 因此返回一个它的实现类SimpleAuthenticationInfo,将user以及获取到的token传入它可以实现自动认证
SimpleAuthenticationInfo simpleAuthenticationInfo=new SimpleAuthenticationInfo(user,accessToken,"");
return simpleAuthenticationInfo;
}
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//从认证那里获取到用户对象User
User user = (User) principals.getPrimaryPrincipal();
//此方法需要一个AuthorizationInfo类型的返回值,因此返回一个它的实现类SimpleAuthorizationInfo
//通过SimpleAuthorizationInfo里的addStringPermission()设置用户的权限
SimpleAuthorizationInfo simpleAuthorizationInfo=new SimpleAuthorizationInfo();
simpleAuthorizationInfo.addStringPermission(user.getRole());
return simpleAuthorizationInfo;
}
自定义异常处理器 MyExceptionHandler.java
//@ControllerAdvice可以实现全局异常处理,可以简单理解为增强了的controller
@ControllerAdvice
public class MyExceptionHandler {
//捕获AuthorizationException的异常
@ExceptionHandler(value = AuthorizationException.class)
@ResponseBody
public Msg handleException(AuthorizationException e) {
Msg msg=Msg.denyAccess("权限不足呀!!!!!");
return msg;
}
}
UserController.java
@RestController
public class UserController {
@Autowired
UserServiceImpl userServiceImpl;
//通过java去操作redis缓存string类型的数据
@Autowired
StringRedisTemplate stringRedisTemplate;
//需要权限为ROLE_USER才能访问/index
@RequiresPermissions("ROLE_USER")
@GetMapping("/index")
public Msg index(@RequestHeader String token){
return Msg.success("index");
}
//需要权限ROLE_ADMIN才能访问hello
@RequiresPermissions("ROLE_ADMIN")
@GetMapping("/hello")
public Msg hello(@RequestHeader String token){
return Msg.success("hello");
}
//登录接口
@PostMapping("/login")
public Msg login(@RequestParam("username")String username,@RequestParam("password")String password){
User user = userServiceImpl.findByUsername(username);
Msg msg=null;
if (user == null) {
msg = Msg.fail("账号错误");
} else if (!password.equals(user.getPassword())) {
msg = Msg.fail("密码错误");
} else {
//通过UUID生成token字符串,并将其以string类型的数据保存在redis缓存中,key为token,value为username
String token= UUID.randomUUID().toString().replaceAll("-","");
stringRedisTemplate.opsForValue().set(token,username,3600,TimeUnit.SECONDS);
msg=Msg.success("登录成功").add("token",token);
}
return msg;
}
//注销接口
@PostMapping("/logout")
public Msg logout(@RequestHeader("token")String token){
//删除redis缓存中的token
stringRedisTemplate.delete(token);
return Msg.success("注销成功");
}
}
application.yml配置文件
server:
port: 80
spring:
datasource:
url: jdbc:mysql://localhost:3306/springsecurity_test?characterEncoding=utf8&serverTimezone=UTC
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
redis:
host: localhost
port: 6379
源代码在https://gitee.com/liu-wenxin/shiro_token_demo.git,通过git clone https://gitee.com/liu-wenxin/shiro_token_demo.git 下载使用