Springboot +Shiro +VUE 前后端分离式权限管理系统

news2024/9/21 19:08:00

前言

前后端分离,一般都是通过token实现,本项目也是一样;用户登录时,生成token及 token过期时间,token与用户是一一对应关系,调用接口的时候,把token放到header或 请求参数中,服务端就知道是谁在调用接口。

代码已上传到Git:

后台代码:https://github.com/FENGZHIJIE1998/shiro-auth

前端代码:https://github.com/FENGZHIJIE1998/shiro-vue

觉得好用的记得点个Star哦

原文链接:https://blog.csdn.net/weixin_42236404/article/details/89319359

第一步:新建工程

第二步:准备好要用的包包和类类

第三步:编写登陆入口

第四步:编写ShiroService中的方法

第五步:编写ShiroConfig类

第六步:实现自定义的AuthenticationToken。

第七步:编写自己的Realm

第八步:实现自定义AuthenticatingFilter。

第九步:详解校验流程

看看效果

第一步:新建工程

pom文件application.yml巴拉巴拉这里省略,这里贴出需要用到的依赖:

        <!--starter-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <!--  test-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!--web-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--validation-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
        <!--JPA-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <!--JDBC-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <!-- shiro-->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.3.2</version>
        </dependency>
        <!--mysql-connector-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <!-- druid-spring-boot-starter -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.10</version>
        </dependency>
        <!-- swagger -->
        <dependency>
            <groupId>com.spring4all</groupId>
            <artifactId>swagger-spring-boot-starter</artifactId>
            <version>1.8.0.RELEASE</version>
        </dependency>
        <!-- knife4j -->
        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>knife4j-spring-boot-starter</artifactId>
            <version>2.0.2</version>
        </dependency>
        <!-- commons-lang -->
        <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
            <version>2.6</version>
        </dependency>

第二步:准备好要用的包包和类类

第三步:编写登陆入口

为了方便这里不做密码加盐加密

/**
 * @Author 大誌
 * @Date 2019/3/30 22:04
 * @Version 1.0
 */
@RestController
public class ShiroController {
 
    private final ShiroService shiroService;
 
    public ShiroController(ShiroService shiroService) {
        this.shiroService = shiroService;
    }
 
 
    /**
     * 登录
     */
    @ApiOperation(value = "登陆", notes = "参数:用户名 密码")
    @PostMapping("/sys/login")
    public Map<String, Object> login(@RequestBody @Validated LoginDTO loginDTO, BindingResult bindingResult) {
        Map<String, Object> result = new HashMap<>();
        if (bindingResult.hasErrors()) {
            result.put("status", 400);
            result.put("msg", bindingResult.getFieldError().getDefaultMessage());
            return result;
        }
 
        String username = loginDTO.getUsername();
        String password = loginDTO.getPassword();
        //用户信息
        User user = shiroService.findByUsername(username);
        //账号不存在、密码错误
        if (user == null || !user.getPassword().equals(password)) {
            result.put("status", 400);
            result.put("msg", "账号或密码有误");
        } else {
            //生成token,并保存到数据库
            result = shiroService.createToken(user.getUserId());
            result.put("status", 200);
            result.put("msg", "登陆成功");
        }
        return result;
    }
 
    /**
     * 退出
     */
    @ApiOperation(value = "登出", notes = "参数:token")
    @PostMapping("/sys/logout")
    public Map<String, Object> logout(@RequestHeader("token")String token) {
        Map<String, Object> result = new HashMap<>();
        shiroService.logout(token);
        result.put("status", 200);
        result.put("msg", "您已安全退出系统");
        return result;
    }
}

第四步:编写ShiroService中的方法

主要是生成一个token返回给前端。

/**
 * @Author 大誌
 * @Date 2019/3/30 22:18
 * @Version 1.0
 */
@Service
public class ShiroServiceImpl implements ShiroService {
 
 
    @Autowired
    private UserRepository userRepository;
    @Autowired
    private SysTokenRepository sysTokenRepository;
 
    /**
     * 根据username查找用户
     *
     * @param username
     * @return User
     */
    @Override
    public User findByUsername(String username) {
        User user = userRepository.findByUsername(username);
        return user;
    }
 
    //12小时后过期
    private final static int EXPIRE = 3600 * 12;
 
    @Override
    /**
     * 生成一个token
     *@param  [userId]
     *@return Result
     */
    public Map<String, Object> createToken(Integer userId) {
        Map<String, Object> result = new HashMap<>();
        //生成一个token
        String token = TokenGenerator.generateValue();
        //当前时间
        Date now = new Date();
        //过期时间
        Date expireTime = new Date(now.getTime() + EXPIRE * 1000);
        //判断是否生成过token
        SysToken tokenEntity = sysTokenRepository.findByUserId(userId);
        if (tokenEntity == null) {
            tokenEntity = new SysToken();
            tokenEntity.setUserId(userId);
            tokenEntity.setToken(token);
            tokenEntity.setUpdateTime(now);
            tokenEntity.setExpireTime(expireTime);
            //保存token
            sysTokenRepository.save(tokenEntity);
        } else {
            tokenEntity.setToken(token);
            tokenEntity.setUpdateTime(now);
            tokenEntity.setExpireTime(expireTime);
            //更新token
            sysTokenRepository.save(tokenEntity);
        }
        result.put("token", token);
        result.put("expire", EXPIRE);
        return result;
    }
 
    @Override
    public void logout(String token) {
        SysToken byToken = findByToken(token);
        //生成一个token
        token = TokenGenerator.generateValue();
        //修改token
        SysToken tokenEntity = new SysToken();
        tokenEntity.setUserId(byToken.getUserId());
        tokenEntity.setToken(token);
        sysTokenRepository.save(tokenEntity);
    }
 
    @Override
    public SysToken findByToken(String accessToken) {
        return sysTokenRepository.findByToken(accessToken);
 
    }
 
    @Override
    public User findByUserId(Integer userId) {
        return userRepository.findByUserId(userId);
    }
}

第五步:编写ShiroConfig类

/**
 * @Author 大誌
 * @Date 2019/3/30 21:50
 * @Version 1.0
 */
@Configuration
public class ShiroConfig {
 
    @Bean("securityManager")
    public SecurityManager securityManager(AuthRealm authRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(authRealm);
        securityManager.setRememberMeManager(null);
        return securityManager;
    }
 
    @Bean("shiroFilter")
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        shiroFilter.setSecurityManager(securityManager);
        //oauth过滤
        Map<String, Filter> filters = new HashMap<>();
        filters.put("auth", new AuthFilter());
        shiroFilter.setFilters(filters);
        Map<String, String> filterMap = new LinkedHashMap<>();
        filterMap.put("/webjars/**", "anon");
        filterMap.put("/druid/**", "anon");
        filterMap.put("/sys/login", "anon");
        filterMap.put("/swagger/**", "anon");
        filterMap.put("/v2/api-docs", "anon");
        filterMap.put("/swagger-ui.html", "anon");
        filterMap.put("/swagger-resources/**", "anon");
        filterMap.put("/**", "auth");
        shiroFilter.setFilterChainDefinitionMap(filterMap);
 
        return shiroFilter;
    }
 
    @Bean("lifecycleBeanPostProcessor")
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }
 
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }
}

第六步:实现自定义的AuthenticationToken。

阅读AuthenticatingFilter抽象类中executeLogin方法,我们发现调用 了subject.login(token),这是shiro的登录方法,且需要token参数,我们自定义 AuthToken类,只要实现AuthenticationToken接口,就可以了。

 //AuthenticatingFilter中的executeLogin()
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
        AuthenticationToken token = createToken(request, response);
        if (token == null) {
            String msg = "createToken method implementation returned null. A valid non-null AuthenticationToken " +
                    "must be created in order to execute a login attempt.";
            throw new IllegalStateException(msg);
        }
        try {
            Subject subject = getSubject(request, response);
            //重点!
            subject.login(token);
            return onLoginSuccess(token, subject, request, response);
        } catch (AuthenticationException e) {
            return onLoginFailure(token, e, request, response);
        }
    }
/**
 * 自定义AuthenticationToken类
 * @Author 大誌
 * @Date 2019/3/31 10:58
 * @Version 1.0
 */
public class AuthToken extends UsernamePasswordToken{
 
    private String token;
 
    public AuthToken(String token) {
        this.token = token;
    }
 
    @Override
    public Object getPrincipal() {
        return token;
    }
 
    @Override
    public Object getCredentials() {
        return token;
    }
}

这里我实现的时候出现了Token不匹配的Bug。DeBug下可以查到源头是代码是用UsernamePasswordToken.class和我自定义的AuthToken.class配对。按道理应该是true,却返回了false...于是我就把自定义的AuthToken不实现AuthenticationToken,转为继承UsernamePasswordToken,就可以了。(renren-fast中却可以,可能是版本的问题)

2020/4/27修改: 为了避免误导,将上诉代码 AuthenticationToken 修改为 UsernamePasswordToken,并且走了一下源码,发现这个getAuthenticationTokenClass()实际上获取到的是UsernamePasswordToken.class

再回头看看renren-fast中的源码,原来他重写了supports方法!

第七步:编写自己的Realm

发起请求时,接受传过来的token后,如何保证token有效及用户权限呢?调用接口时,接受传过来的token后,如何保证token有效及用户权限呢?其实,Shiro提供了AuthorizingRealm以及AuthenticatingFilter抽象类,继承AuthorizingRealm和AuthenticatingFilter抽象类重写方法即可

/**
 * @Author 大誌
 * @Date 2019/3/30 21:38
 * @Version 1.0
 */
@Component
public class AuthRealm extends AuthorizingRealm {
 
    @Autowired
    private ShiroService shiroService;
 
    @Override
    /**
     * 授权 获取用户的角色和权限
     *@param  [principals]
     *@return org.apache.shiro.authz.AuthorizationInfo
     */
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        //1. 从 PrincipalCollection 中来获取登录用户的信息
        User user = (User) principals.getPrimaryPrincipal();
        //Integer userId = user.getUserId();
        //2.添加角色和权限
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        for (Role role : user.getRoles()) {
            //2.1添加角色
            simpleAuthorizationInfo.addRole(role.getRoleName());
            for (Permission permission : role.getPermissions()) {
                //2.1.1添加权限
                simpleAuthorizationInfo.addStringPermission(permission.getPermission());
            }
        }
        return simpleAuthorizationInfo;
    }
 
    @Override
    /**
     * 认证 判断token的有效性
     *@param  [token]
     *@return org.apache.shiro.authc.AuthenticationInfo
     */
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        //获取token,既前端传入的token
        String accessToken = (String) token.getPrincipal();
        //1. 根据accessToken,查询用户信息
        SysToken tokenEntity = shiroService.findByToken(accessToken);
        //2. token失效
        if (tokenEntity == null || tokenEntity.getExpireTime().getTime() < System.currentTimeMillis()) {
            throw new IncorrectCredentialsException("token失效,请重新登录");
        }
        //3. 调用数据库的方法, 从数据库中查询 username 对应的用户记录
        User user = shiroService.findByUserId(tokenEntity.getUserId());
        //4. 若用户不存在, 则可以抛出 UnknownAccountException 异常
        if (user == null) {
            throw new UnknownAccountException("用户不存在!");
        }
        //5. 根据用户的情况, 来构建 AuthenticationInfo 对象并返回. 通常使用的实现类为: SimpleAuthenticationInfo
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, accessToken, this.getName());
        return info;
    }
}

第八步:实现自定义AuthenticatingFilter。

/**
 * Shiro自定义auth过滤器
 *
 * @Author 大誌
 * @Date 2019/3/31 10:38
 * @Version 1.0
 */
@Component
public class AuthFilter extends AuthenticatingFilter {
 
 
    // 定义jackson对象
    private static final ObjectMapper MAPPER = new ObjectMapper();
 
    /**
     * 生成自定义token
     *
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    @Override
    protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
        //获取请求token
        String token = TokenUtil.getRequestToken((HttpServletRequest) request);
 
        return new AuthToken(token);
    }
 
    /**
     * 步骤1.所有请求全部拒绝访问
     *
     * @param request
     * @param response
     * @param mappedValue
     * @return
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        if (((HttpServletRequest) request).getMethod().equals(RequestMethod.OPTIONS.name())) {
            return true;
        }
        return false;
    }
 
    /**
     * 步骤2,拒绝访问的请求,会调用onAccessDenied方法,onAccessDenied方法先获取 token,再调用executeLogin方法
     *
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        //获取请求token,如果token不存在,直接返回
        String token = TokenUtil.getRequestToken((HttpServletRequest) request);
        if (StringUtils.isBlank(token)) {
            HttpServletResponse httpResponse = (HttpServletResponse) response;
            httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
            httpResponse.setHeader("Access-Control-Allow-Origin", HttpContextUtil.getOrigin());
            httpResponse.setCharacterEncoding("UTF-8");
            Map<String, Object> result = new HashMap<>();
            result.put("status", 400);
            result.put("msg", "请先登录");
            String json = MAPPER.writeValueAsString(result);
            httpResponse.getWriter().print(json);
            return false;
        }
        return executeLogin(request, response);
    }
 
    /**
     * token失效时候调用
     */
    @Override
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        httpResponse.setContentType("application/json;charset=utf-8");
        httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
        httpResponse.setHeader("Access-Control-Allow-Origin", HttpContextUtil.getOrigin());
        httpResponse.setCharacterEncoding("UTF-8");
        try {
            //处理登录失败的异常
            Throwable throwable = e.getCause() == null ? e : e.getCause();
            Map<String, Object> result = new HashMap<>();
            result.put("status", 400);
            result.put("msg", "登录凭证已失效,请重新登录");
            String json = MAPPER.writeValueAsString(result);
            httpResponse.getWriter().print(json);
        } catch (IOException e1) {
        }
        return false;
    }
 
}

第九步:详解校验流程

先给你们上一个超级详细的流程图。

接着我们打上断点按照代码走走,可能会有点啰嗦。

1. 前端发起请求首先会进入AuthFilter的 isAccessAllowed(),除了OPTION方法,其余都拦截。

2. 拦截之后进入AuthFilter的onAccessDenied(),这里获取token后判断token是否isBlank。如果是,代表请求未携带token,直接默认返回400,未登录给前端,流程就结束了。如果携带了token则进入第三步,继续流程。

3. 接着进入AuthFilter的createToken,这里生成我们自定义的AuthToken对象。

4. 接着就会来到AuthRealm中的doGetAuthenticationInfo(),在这个方法中继续token的有效性校验,例如过期、和数据库的token对不上(用户已退出)的情况。如果校验失败,进入第5步,否则进入第6步。

5. token失效后回到AuthFilter中的onLoginFailure(),返回400以及msg,流程结束。

6. Token校验成功后进入AuthRealm的doGetAuthorizationInfo(),进行获取当前用户拥有的权限,之后底层代码会进行权限验证。如果用户有权限则会进入请求方法,否则抛出异常。到这一步校验过程就结束了。

看看效果

终于熬完上面的步骤了,这时候总体的架构已经确立好了,下面让我们来看看效果如何

DTO

/**
 * 登录传输类
 */
@Data
public class LoginDTO {
    @NotBlank(message = "用户名不能为空")
    private String username;
    @NotBlank(message = "密码不能为空")
    private String password;
}

实体类

@Getter
@Setter
@Entity
public class User {
    @Id
    private Integer userId;
 
    private String username;
    private String password;
 
    @ManyToMany(fetch = FetchType.EAGER)
    @JoinTable(name = "user_role",
            joinColumns = {@JoinColumn(name = "USER_ID", referencedColumnName = "userId")},
            inverseJoinColumns = {@JoinColumn(name = "ROLE_ID", referencedColumnName = "roleId")})
    private Set<Role> roles;
 
}
 
@Getter
@Setter
@Entity
public class Role {
 
    @Id
    private Integer roleId;
    private String roleName;
 
    @ManyToMany(fetch = FetchType.EAGER)
    @JoinTable(name = "role_permission",
            joinColumns = {@JoinColumn(name = "ROLE_ID", referencedColumnName = "roleId")},
            inverseJoinColumns = {@JoinColumn(name = "PERMISSION_ID", referencedColumnName = "permissionId")})
    private Set<Permission> permissions;
}
 
@Getter
@Setter
@Entity
public class Permission {
 
    @Id
    private Integer permissionId;
    private String permissionName;
    private String permission;
}
 
@Getter
@Setter
@Entity
public class SysToken{
 
    @Id
    private Integer userId;
    private String token;
    private Date expireTime;
    private Date updateTime
}

以及给实体类附上权限:

我定义了三个用户

用户    角色    权限
Jack    SVIP    select;save;delete;update
Rose    VIP    select;save;update
Paul    P    select
/*
Navicat MySQL Data Transfer
Source Server         : localhost
Source Server Version : 50549
Source Host           : localhost:3306
Source Database       : shiro
Target Server Type    : MYSQL
Target Server Version : 50549
File Encoding         : 65001
Date: 2019-04-07 17:06:36
*/
 
SET FOREIGN_KEY_CHECKS=0;
 
-- ----------------------------
-- Table structure for permission
-- ----------------------------
DROP TABLE IF EXISTS `permission`;
CREATE TABLE `permission` (
  `permission_id` int(11) NOT NULL,
  `permission` varchar(255) DEFAULT NULL,
  `permission_name` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`permission_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 
-- ----------------------------
-- Records of permission
-- ----------------------------
INSERT INTO `permission` VALUES ('1', 'select', '查看');
INSERT INTO `permission` VALUES ('2', 'update', '更新');
INSERT INTO `permission` VALUES ('3', 'delete', '删除');
INSERT INTO `permission` VALUES ('4', 'save', '新增');
 
-- ----------------------------
-- Table structure for role
-- ----------------------------
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (
  `role_id` int(11) NOT NULL,
  `role_name` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 
-- ----------------------------
-- Records of role
-- ----------------------------
INSERT INTO `role` VALUES ('1', 'svip');
INSERT INTO `role` VALUES ('2', 'vip');
INSERT INTO `role` VALUES ('3', 'p');
 
-- ----------------------------
-- Table structure for role_permission
-- ----------------------------
DROP TABLE IF EXISTS `role_permission`;
CREATE TABLE `role_permission` (
  `role_id` int(11) NOT NULL,
  `permission_id` int(11) NOT NULL,
  PRIMARY KEY (`role_id`,`permission_id`),
  KEY `FKf8yllw1ecvwqy3ehyxawqa1qp` (`permission_id`),
  CONSTRAINT `FKa6jx8n8xkesmjmv6jqug6bg68` FOREIGN KEY (`role_id`) REFERENCES `role` (`role_id`),
  CONSTRAINT `FKf8yllw1ecvwqy3ehyxawqa1qp` FOREIGN KEY (`permission_id`) REFERENCES `permission` (`permission_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 
-- ----------------------------
-- Records of role_permission
-- ----------------------------
INSERT INTO `role_permission` VALUES ('1', '1');
INSERT INTO `role_permission` VALUES ('2', '1');
INSERT INTO `role_permission` VALUES ('3', '1');
INSERT INTO `role_permission` VALUES ('1', '2');
INSERT INTO `role_permission` VALUES ('2', '2');
INSERT INTO `role_permission` VALUES ('1', '3');
INSERT INTO `role_permission` VALUES ('1', '4');
INSERT INTO `role_permission` VALUES ('2', '4');
 
-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
  `user_id` int(11) NOT NULL,
  `password` varchar(255) DEFAULT NULL,
  `username` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 
-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES ('1', '123', 'Jack');
INSERT INTO `user` VALUES ('2', '123', 'Rose');
INSERT INTO `user` VALUES ('3', '123', 'Paul');
 
-- ----------------------------
-- Table structure for user_role
-- ----------------------------
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role` (
  `user_id` int(11) NOT NULL,
  `role_id` int(11) NOT NULL,
  PRIMARY KEY (`user_id`,`role_id`),
  KEY `FKa68196081fvovjhkek5m97n3y` (`role_id`),
  CONSTRAINT `FK859n2jvi8ivhui0rl0esws6o` FOREIGN KEY (`user_id`) REFERENCES `user` (`user_id`),
  CONSTRAINT `FKa68196081fvovjhkek5m97n3y` FOREIGN KEY (`role_id`) REFERENCES `role` (`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 
-- ----------------------------
-- Table structure for sys_token
-- ----------------------------
CREATE TABLE `sys_token` (
  `user_id` int(11) NOT NULL,
  `expire_time` datetime DEFAULT NULL,
  `token` varchar(255) DEFAULT NULL,
  `update_time` datetime DEFAULT NULL,
  PRIMARY KEY (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 
 
 
-- ----------------------------
-- Records of user_role
-- ----------------------------
INSERT INTO `user_role` VALUES ('1', '1');
INSERT INTO `user_role` VALUES ('2', '2');
INSERT INTO `user_role` VALUES ('3', '3');

测试类:因为我是用Swagger来测试,所以为了方便就直接传递token参数。具体开发时候可由前端把接收到的token放入Header。

/**
 * @Author 大誌
 * @Date 2019/4/7 15:20
 * @Version 1.0
 */
@RestController
public class TestController {
 
    @RequiresPermissions({"save"}) //没有的话 AuthorizationException
    @PostMapping("/save")
    public Map<String, Object> save(String token) {
        System.out.println("save");
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("status", 200);
        map.put("msg", "当前用户有save的权力");
        return map;
    }
 
    @RequiresPermissions({"delete"}) //没有的话 AuthorizationException
    @DeleteMapping("/delete")
    public Map<String, Object> delete(String token) {
        System.out.println("delete");
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("status", 200);
        map.put("msg", "当前用户有delete的权力");
        return map;
    }
 
    @RequiresPermissions({"update"}) //没有的话 AuthorizationException
    @PutMapping("update")
    public Map<String, Object> update(String token) {
        System.out.println("update");
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("status", 200);
        map.put("msg", "当前用户有update的权力");
        return map;
    }
 
    @RequiresPermissions({"select"}) //没有的话 AuthorizationException
    @GetMapping("select")
    public Map<String, Object> select(String token, HttpSession session) {
        System.out.println("select");
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("status", 200);
        map.put("msg", "当前用户有select的权力");
        return map;
    }
 
    @RequiresRoles({"vip"}) //没有的话 AuthorizationException
    @GetMapping("/vip")
    public Map<String, Object> vip(String token) {
        System.out.println("vip");
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("status", 200);
        map.put("msg", "当前用户有VIP角色");
        return map;
    }
    @RequiresRoles({"svip"}) //没有的话 AuthorizationException
    @GetMapping("/svip")
    public Map<String, Object> svip(String token) {
        System.out.println("svip");
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("status", 200);
        map.put("msg", "当前用户有SVIP角色");
        return map;
    }
    @RequiresRoles({"p"}) //没有的话 AuthorizationException
    @GetMapping("/p")
    public Map<String, Object> p(String token) {
        System.out.println("p");
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("status", 200);
        map.put("msg", "当前用户有P角色");
        return map;
    }
}

ExceptionHandler 异常处理器,用于捕获无权限时候的异常

@ControllerAdvice
public class MyExceptionHandler {
 
    @ExceptionHandler(value = AuthorizationException.class)
    @ResponseBody
    public Map<String, String> handleException(AuthorizationException e) {
        //e.printStackTrace();
        Map<String, String> result = new HashMap<String, String>();
        result.put("status", "400");
        //获取错误中中括号的内容
        String message = e.getMessage();
        String msg=message.substring(message.indexOf("[")+1,message.indexOf("]"));
        //判断是角色错误还是权限错误
        if (message.contains("role")) {
            result.put("msg", "对不起,您没有" + msg + "角色");
        } else if (message.contains("permission")) {
            result.put("msg", "对不起,您没有" + msg + "权限");
        } else {
            result.put("msg", "对不起,您的权限有误");
        }
        return result;
    }
}

启动项目来看看效果: 访问 localhost:9090/shiro/doc.html

登陆失败:

登陆成功:

登录成功后会返回token,记得带上token访问以下接口

有某个角色时候:

没有某个角色的时候:

有某个权力时候:

没有某个权力的时候:

退出系统

原本的token就失效了,我们再访问原本可以访问的接口看看

至此就已经进入尾声了

2020/3/27 新编写了VUE+Element前端页面

正常访问:

非法访问:

重点:当未登录时候访问项目内部页面,由前端控制路由返回登录页,并不会出现可恶的login.jsp,这里我们故意改变数据库token来展示效果。

总结

至于最后没有权利或角色返回的json字符串是因为他抛出AuthorizationException。可以自定义全局异常处理器进行处理。通过这种token达到即可达到前后端分离开发。各位客官,点个赞吧qaq。

2019/11/26日修改:在后续开发中,发现shiro如果使用ShiroConfig中shiroFiltet的map进行权限或角色拦截,会出现只走登陆认证,不走授权认证的情况。这是个巨坑!后续再写一篇文章深究一下。解决方法:使用注解@RequiresRoles() 以及@RequiresPermissions()进行权限和角色拦截

 
    @Bean("shiroFilter")
    public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        shiroFilter.setSecurityManager(securityManager);
        //自定义过滤(关键)
        Map<String, Filter> filters = new HashMap<>();
        filters.put("auth", new AuthFilter());
        shiroFilter.setFilters(filters);
        Map<String, String> filterMap = new LinkedHashMap<>();
        //主要是这部分: 不要用这种方法,最好用注解的方法
        filterMap.put("/add", "roles[admin]");
        filterMap.put("/list", "roles[admin,user]");
        filterMap.put("/delete", "perms[admin:delete]");
 
        filterMap.put("/**", "auth");
        shiroFilter.setFilterChainDefinitionMap(filterMap);
 
        return shiroFilter;
    }

2020/3/25补充,修改了部分不符合规范的代码,添加了全局异常捕获器。同时补充了校验流程。同时提示两句,因为token频繁在客户端和服务器端传输,因此可能会造成token劫持攻击(既黑客捕获你的token之后就可以代替你为所欲为),如果对这方面有安全隐患的担忧,可以采取每访问一次接口,更新一次token。并且我这里处于方便的原因是采用了mysql存储token,具体开发中应该用redis缓存来存储。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/172143.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ROS2机器人编程简述humble-第二章-Publishing and Subscribing .3.2

ROS2机器人编程简述humble-第二章-Controlling the Iterative Execution .3.1官方示例pub和sub使用std_msgs/msg/string.hpp&#xff0c;数据类型std_msgs::msg::String。这本书中使用是std_msgs/msg/int32.hpp&#xff0c;数据类型&#xff1a;std_msgs::msg::Int32。对于机器…

Servlet —— Servlet API

JavaEE传送门JavaEE Servlet —— Tomcat, 初学 Servlet 程序 Servlet —— Smart Tomcat,以及一些访问出错可能的原因 目录Servlet APIHttpServletHttpServletRequest获取 GET 请求中的参数获取 POST 请求的参数HttpServletResponseServlet API 虽然 Servlet 提供的类和方法…

【UE4 第一人称射击游戏】53-制作烟雾弹

上一篇&#xff1a;【UE4 第一人称射击游戏】52-手榴弹攻击丧尸本篇效果&#xff1a;按F键掷出烟雾弹&#xff0c;伴随产生音效和烟雾效果本篇步骤&#xff1a;拷贝一份“GrenadeActor”命名为“SmokeGrenadeActor”双击打开“SmokeGrenadeActor”&#xff0c;删除如下节点改变…

尚医通-医院详情功能(二十七)

目录&#xff1a; &#xff08;1&#xff09;前台用户系统-医院详请-情接口开发 &#xff08;2&#xff09;前台用户系统-技术点-nuxt路由 &#xff08;3&#xff09;前台用户系统-医院详情-前端整合 &#xff08;1&#xff09;前台用户系统-医院详-情接口开发 现在做在页面…

13 Java异常(异常过程解析、throw、throws、try-catch关键字)

文章目录13 异常13.1 异常概念13.2 异常的产生过程解析13.3 异常的阐释和处理13.3.1 throw关键字13.3.2 throws关键字13.3.3 try-catch代码块13.3.4 try-catch-finally代码块Java中final、finally和finalize相似点和区别13.3.5 自定义异常13 异常 13.1 异常概念 异常&#xff…

SpringCloud(12):Zuul路由网关

1 为什么需要服务网关 在分布式系统系统中&#xff0c;有商品、订单、用户、广告、支付等等一大批的服务&#xff0c;前端怎么调用呢&#xff1f;和每个服务一个个打交道&#xff1f;这显然是不可能的&#xff0c;这就需要有一个角色充当所有请求的入口&#xff0c;这个角色就是…

【C++】从0到1入门C++编程学习笔记 - 实战篇:通讯录管理系统

文章目录一、需求分析二、创建项目2.1 创建新项目2.1 添加文件三、菜单功能四、退出功能五、添加联系人5.1 设计联系人结构体5.2 设计通讯录结构体5.3 main 函数中创建通讯录5.4 封装添加联系人函数5.5 测试添加联系人功能六、显示联系人6.1 封装显示联系人函数6.2 测试显示联系…

STM32F103和AIR32F103的FreeRTOS中断优先级

关于 Arm Cortex M 系列内核的中断优先级 Cortex M 的中断和优先级 首先要区分开 中断 和 中断优先级 这是两个不同的东西, 不要搞混了 对于 Cortex-M0 和 Cortex-M0 内核, 除了系统内建中断外, 支持最多 32 个中断对于 Cortex-M3 内核, 除了 16 个内核中断外, 支持最多 240…

前端初学者的Ant Design Pro V6总结(上)

前端初学者的Ant Design Pro V6总结&#xff08;上&#xff09; 一、UI组件开发流程 () > {} 通用&#xff08;异步&#xff09;函数useEmotionCss 定义CSSuseModel获取全局状态useCallback 处理React合成事件JSX 拆分组件initiateState 中CurrentUser空值处理initiateSta…

8. 数字类型讲解

python3 数字类型的使用 1. 基础知识 Python 数字数据类型用于存储数值。 数据类型是不允许改变的,这就意味着如果改变数字数据类型的值&#xff0c;将重新分配内存空间。 可以使用del语句删除一些数字对象的引用。 del var1[,var2[,var3[....,varN]]]Python 支持三种不同的…

计算机视觉算法——基于深度学习的高精地图算法(HDMapNet / VectorMapNet / MapTR / VectorNet)

计算机视觉算法——基于深度学习的高精地图算法&#xff08;HDMapNet / VectorMapNet / MapTR / VectorNet&#xff09;计算机视觉算法——基于深度学习的高精地图算法&#xff08;HDMapNet / VectorMapNet / MapTR / VectorNet&#xff09;1. HDMapNet1.1 网络结构及特点1.1.1…

SAP SD 自定义销售订单审批状态

自定义销售订单审批状态 销售订单可以在其抬头或者项目中定义审批状态&#xff0c;一般在抬头定义的话就相当于针对整单的审批&#xff0c;可以实现多级审批&#xff0c;每级审批设置能进行何种操作&#xff0c;这里就需要在IMG中定义审批状态参数文件。 一、定义状态参数文件…

[精] MySQL和Oracle下Mybatis批量操作示例和获取影响行数介绍

手打不易&#xff0c;如果转摘&#xff0c;请注明出处&#xff01; 注明原文&#xff1a;https://zhangxiaofan.blog.csdn.net/article/details/117933877 目录 前言 Mybatis 执行器 表结构定义 Mybatis批量新增 批量新增——Mysql写法 批量新增——Oracle写法 Mybatis批…

【甄选靶场】Vulnhub百个项目渗透——项目五十四:jarbas-1(类git反弹shell,计划任务提权)

Vulnhub百个项目渗透 Vulnhub百个项目渗透——项目五十四&#xff1a;jarbas-1&#xff08;类git反弹shell,计划任务提权&#xff09; &#x1f525;系列专栏&#xff1a;Vulnhub百个项目渗透 &#x1f389;欢迎关注&#x1f50e;点赞&#x1f44d;收藏⭐️留言&#x1f4dd; …

SpringMvc源码分析(四) 请求执行过程之执行MethodHandler

在上文SpringMvc源码分析&#xff08;三&#xff09;中我们分析了DispatcherServlet类中的doDispatcher方法&#xff0c; 并通过分析方法1和方法2了解了请求执行时是怎么获取MethodHandler链的&#xff0c;本文接上文继续分析方法3、方法4和方法5了解MethodHandler是如何执行的…

springboot rabbitmq 非阻塞重试机制实现

重试的应用场景 比如&#xff0c;系统之间同步数据&#xff0c;A系统发送数据给B系统&#xff0c;因为网络原因或者B系统正在重启&#xff0c;可能收不到信息&#xff0c;为了确保B能收到消息就得重试几次&#xff1b;经典的比如&#xff0c;微信支付回调 对后台通知交互时&am…

VScode远程连接Linux

文章目录一、下载安装二、使用三、连接四、基本操作五、VScode内置命令行六、推荐插件一、下载安装 下载的问题就不用多说了把&#xff0c;可能存在的问题就是下载的速度比较慢 前往官网进行下载&#xff1a;前往官网找到适合自己的版本&#xff1a; 但是由于官网是国外的&am…

DT-6_TTL-WiFi透传模块介绍

DT-6_TTL-WiFi透传模块简介TTL-WiFi模块基于ESP-M2WiFi 模块研发&#xff0c;引出串口TTL、EN、STATE等引脚。产品内置我司最新版本的串口透传固件可完成设备TTL 端口到WiFi/云的数据实时透传&#xff0c;具备低功耗控制&#xff0c;状态指示等功能。本模块可直接取代原有的有线…

【手写 Vue2.x 源码】第三十二篇 - diff算法-乱序比对

一&#xff0c;前言 上篇&#xff0c;diff算法-比对优化&#xff08;下&#xff09;&#xff0c;主要涉及以下几个点&#xff1a; 介绍了儿子节点比较的流程介绍并实现了头头、尾尾、头尾、尾头4种特殊情况比对 本篇&#xff0c;继续介绍 diff算法-乱序比对 二&#xff0c;乱…

MATLAB | 全网最全边际图绘制模板(直方图、小提琴图、箱线图、雨云图、散点图... ...)

如标题所言&#xff0c;这应该是全网最全的边际图绘制模板&#xff0c;中心图有8种格式&#xff0c;边际图有11种格式&#xff0c;共计88种组合&#xff0c;另外模板中给了8款配色&#xff0c;我愿称其为888组合&#xff0c;只需要更换一下数据就能绘制出各种类型的边际图: 甚至…