-
spring boot+vue前后端分离
2020-12-29 13:22:23spring boot+vue前后端分离 -
springboot+vue前后端分离开发项目源码
2021-01-02 21:32:54springboot+vue前后端分离开发项目源码 -
SpringBoot+VUE前后端分离demo
2020-04-20 13:45:10SpringBoot+VUE前后端分离demo,后端 Spring Boot 技术和前端 Vue 技术来简单开发一个增删改查demo,该demo以简单、方便理解的方式来记录前后端结合使用的过程,方便正式开发复杂项目时能提前整体理解流程 -
Flask-Vue前后端分离
2018-07-23 20:42:44Flask-Vue前后端分离 最近学习了一下前后端分离技术,前端用的是Vue,后段用的是Flask。 vue官网:开源的 Javascript 框架,vue是一套构建用户界面的渐进式框架,Vue采用自底向上的增量开发设计。vue是轻量级的,...Flask-Vue前后端分离
最近学习了一下前后端分离技术,前端用的是Vue,后段用的是Flask。
vue官网:开源的 Javascript 框架,vue是一套构建用户界面的渐进式框架,Vue采用自底向上的增量开发设计。vue是轻量级的,有很多独立的功能和库。
vue.js满足当前webapp项目的开发需求,MVVM框架诞生,而Vue.js便是这样一种js框架。两大核心:数据驱动和组件化。
Flask:用Python实现,是一个web框架,属于微框架。1、主要依赖版本
- Python : V3.6.4
- Vue: V2.9.6
- Vue-CLI: V2.9.6
- node: v10.6.0
- npm: V6.1.0
- Flask: V1.0.2
- Flask-Cors: V3.0.6
- bootstrap: V4.0.0(最新版4.1.2不兼容)
以下项目的创建都是在Python虚拟环境和npm局部安装的,只有Vue-CLI是全局安装的。
npm ls -g --depth 0 查找npm全局安装的包
2、构建Python虚拟环境
防止依赖的混乱,首先创建虚拟环境。
Mac:code hubo$ mkdir flask-vue-crud Mac:code hubo$ cd flask-vue-crud/ Mac:flask-vue-crud$ python -m venv venv Mac:flask-vue-crud$ source venv/bin/activate (venv) Mac:flask-vue-crud hubo$ ls venv
第一行创建flask-vue-crud文件夹,第三行创建python虚拟环境python3.3版本可以通过venv模块,python2版本可以使用virtualenv模块。
第四行激活虚拟环境。- Windows:source venv/Scripts/activate.bat
- Linux:source venv/bin/activate
3、创建Flask项目
首先安装Flask和Flask-CORS扩展。
Flask—CORS:
如果使用不同的协议,或者请求来自于其他的 IP 地址或域名或端口,就需要用到 Cross Origin Resource Sharing (CORS),这正是 Flask-CORS 扩展帮我们做到的。实际环境中只配置来自前端应用所在的域的请求。(venv) Mac:flask-vue-crud hubo$ pip install Flask Flask-Cors
然后在根目录下创建app.py文件:
from flask import Flask, jsonify from flask_cors import CORS app = Flask(__name__) CORS(app) @app.route('/', methods=['GET']) def ping_pong(): return jsonify('Hello World!') #(jsonify返回一个json格式的数据) if __name__ == '__main__': app.run()
然后执行:
(venv) Mac:flask-vue-crud hubo$ python app.py
打开浏览器,输入http://localhost:5000/ping,会看到输出
Hello World!
目前Flask环境已经配置好,并且已经创建了一个Flask小Demo。
4、Vue Setup
Vue CLI:官方发布 vue.js 项目脚手架,使用 vue-cli 可以快速创建 vue 项目。
首先全局安装vue Cli:npm install -g(全局安装),npm下载的国外的依赖,可以使用淘宝的cnpm。
下载淘宝的cnpmnpm install -g cnpm --registry=https://registry.npm.taobao.org
使用npm或cnom下载vue cli
(venv) Mac:flask-vue-crud hubo$ npm insatll -g vue-cli
查看安装成功
Mac:~ hubo$ vue -V 2.9.6
接下来通过webpack初始化一个新的Vue项目client:
Mac:flask-vue-crud hubo$ vue init webpack client ? Project name client ? Project description A Vue.js project ? Author hubo <1290259791@qq.com> ? Vue build standalone ? Install vue-router? Yes ? Use ESLint to lint your code? Yes ? Pick an ESLint preset Airbnb ? Set up unit tests No ? Setup e2e tests with Nightwatch? No ? Should we run `npm install` for you after the project has been created? (recom mended) npm
主要写代码的位置:/flask-vue-crud/client/src/,目录结构如下所示:
. ├── App.vue ├── assets │ └── logo.png ├── components │ ├── HelloWorld.vue │ └── Ping.vue ├── main.js └── router └── index.js
各个目录的意思:
名字 作用 main.js app 的入口,它会加载和初始化 Vue 和根组件 App.vue 根组件 - 其他组件的入口 assets 静态文件如图片和字体等都放在这里 components UI 组件放在这里 router URL 同组件的映射关系在这里定义 client/src/components/HelloWord.vue是自动生成的单文件组件,有三部分组成:
- template: 组件的 HTML 内容
- script: 组件的逻辑代码(Javascript)
- style: 组件的样式(CSS)
运行该程序:
(venv) Mac:flask-vue-crud hubo$ cd client/ (venv) Mac:client hubo$ ls README.md index.html package.json build node_modules src config package-lock.json static (venv) Mac:client hubo$ npm run dev 启动dev
访问http://localhost:8080 能看到前端页面
5、添加一个新组件
创建client/src/components/Ping.vue:
<template> <div> <p>{{ msg }}</p> </div> </template> <script> export default { name: 'Ping', data() { return { msg: 'Hello!', }; }, }; </script>
更新client/src/router/index.js,映射/ping到Ping组件:
import Vue from 'vue'; import Router from 'vue-router'; import HelloWorld from '@/components/HelloWorld'; import Ping from '@/components/Ping'; Vue.use(Router); export default new Router({ routes: [ { path: '/', name: 'HelloWorld', component: HelloWorld, }, { path: '/ping', name: 'Ping', component: Ping, }, ], mode: 'history', });
上面的mode: 'history’是为了让 URL 变成http://localhost:8080/ping的形式。如果,不加该设置,默认的 URL 为http://localhost:8080/#/ping的形式。
访问http://localhost:8080/ping 能看到Hello!
6、连接前后段
通过axios发送AJAX请求,安装axios:
(venv) Mac:client hubo$ cnpm install axios --save ✔ Installed 1 packages ✔ Linked 4 latest versions ✔ Run 0 scripts ✔ All packages installed (5 packages installed from npm registry, used 1s, speed 12.01kB/s, json 5(12.96kB), tarball 0B)
更新Ping.vue组件,代码如下。
<template> <div class="container"> <button type="button" class="btn btn-primary">{{ msg }}</button> </div> </template> <script> import axios from 'axios'; export default { name: 'Ping', data() { return { msg: 'Hello World!', }; }, methods: { getMessage() { const path = 'http://localhost:5000/ping'; axios.get(path) .then((res) => { this.msg = res.data; }) .catch((error) => { // eslint-disable-next-line console.error(error); }); }, }, created() { this.getMessage(); }, }; </script>
启动后端 Flask 应用,访问http://localhost:8080/ping,会看到页面会呈现后端返回的数据。
将后段的数据改为:{1:‘a’,2:‘b’,3:‘c’}
启动Flask:(venv) Mac:flask-vue-crud hubo$ python app.py * Serving Flask app "app" (lazy loading) * Environment: production WARNING: Do not use the development server in a production environment. Use a production WSGI server instead. * Debug mode: on * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) * Restarting with stat * Debugger is active! * Debugger PIN: 239-628-485
启动dec:
(venv) Mac:client hubo$ npm run dev
7、引入CSS框架bootstrap
引入 CSS 框架 bootstrap,通过如下命令安装bootstrap:使用的是cnpm
下面的cnpm都是局部安装。(venv) Mac:client hubo$ cnpm install bootstrap@4.0.0 --save ✔ Installed 1 packages ✔ Linked 0 latest versions ✔ Run 0 scripts peerDependencies WARNING bootstrap@4.0.0 requires a peer of jquery@1.9.1 - 3 but none was installed peerDependencies WARNING bootstrap@4.0.0 requires a peer of popper.js@^1.12.9 but none was installed ✔ All packages installed (1 packages installed from npm registry, used 770ms, speed 6.85kB/s, json 1(5.27kB), tarball 0B)
我这里缺少两个依赖,继续下载依赖
(venv) Mac:client hubo$ cnpm install jquery@1.9.1 --save ✔ Installed 1 packages ✔ Linked 0 latest versions ✔ Run 0 scripts ✔ All packages installed (1 packages installed from npm registry, used 1s, speed 335.44kB/s, json 1(5.19kB), tarball 482.88kB) (venv) Mac:client hubo$ cnpm install popper.js --save ✔ Installed 1 packages ✔ Linked 0 latest versions ✔ Run 0 scripts ✔ All packages installed (1 packages installed from npm registry, used 543ms, speed 16.56kB/s, json 1(8.99kB), tarball 0B)
再次下载bootstrap:
(venv) Mac:client hubo$ cnpm install bootstrap@4.0.0 --save ✔ Installed 1 packages ✔ Linked 0 latest versions ✔ Run 0 scripts ✔ All packages installed (used 647ms, speed 41.65kB/s, json 1(26.95kB), tarball 0B)
在 app 的入口文件client/src/main.js中导入 bootstrap:
import 'bootstrap/dist/css/bootstrap.css'; import Vue from 'vue'; import App from './App'; import router from './router'; Vue.config.productionTip = false; /* eslint-disable no-new */ new Vue({ el: '#app', router, components: { App }, template: '<App/>', });
去掉根组件client/src/App.vue中多余的样式:
<style> #app { margin-top: 60px; } </style>
在Ping.vue组件中增加样式:
<template> <div class="container"> <button type="button" class="btn btn-primary">{{ msg }}</button> </div> </template>
再次访问http://localhost:8080/ping查看效果。
8、遇到的问题
- Newline required at end of file but not found
这种错误处理方法:
我的错误是在33行后面添加一个空白行就可以了。 - bootstrap最新版的好像和vue不兼容,使用的是4.0.0版本。
- 有遇到其他的问题,可以留言板讨论,目前除了bootstrap是4.0版本其他的应该都是最新的版本。
-
一看就懂!Springboot +Shiro +VUE 前后端分离式权限管理系统
2019-04-15 20:40:28前段日子写过一篇关于SpringBoot+Shiro的简单整合的例子,那个例子并不适用于我们目前的前后端分离开发的趋势。我之前写过一个项目也是用到了Shiro的前后端分离,某度了许久也没找到解决方案,什么去掉shiroFilter....目录
第六步:实现自定义的AuthenticationToken。
第八步:实现自定义AuthenticatingFilter。
前言
前段日子写过一篇关于SpringBoot+Shiro的简单整合的例子,那个例子并不适用于我们目前的前后端分离开发的趋势。我之前写过一个项目也是用到了Shiro的前后端分离,某度了许久也没找到解决方案,什么去掉shiroFilter.setLoginUrl();也阻止不了讨人厌的login.jsp的出现。直到我看到了renren-fast的源码...废话不多说,让我们来看看如何实现吧!
前后端分离
要实现前后端分离,需要考虑以下2个问题: 1. 项目不再基于session了,如何知道访问者是谁? 2. 如何确认访问者的权限?
前后端分离,一般都是通过token实现,本项目也是一样;用户登录时,生成token及 token过期时间,token与用户是一一对应关系,调用接口的时候,把token放到header或 请求参数中,服务端就知道是谁在调用接口。代码已上传到Git:
后台代码:https://github.com/FENGZHIJIE1998/shiro-auth
前端代码:https://github.com/FENGZHIJIE1998/shiro-vue
觉得好用的记得点个Star哦
Let's do it!!
介绍:这次我们使用Shiro快速搭建前后端分离的权限管理系统 利用JPA帮我们管理数据库,Swagger Knife4j 帮我搭建Web测试环境;后台基于 Springboot JPA Knife4j Shiro
前端基于 VUE ElementUI
注意:主要观察token的使用方法!
第一步:新建工程
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("/test") 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缓存来存储。
有什么问题可以评论或者私信我,每日在线解(LIAO)疑(SAO)。
我是大誌,一位准备996的卑微码农🐶,觉得好用记得点赞收藏!!!
-
阿里云 django+vue前后端分离汇总
2020-12-10 16:33:08阿里云 django+vue前后端分离 前端打包上传 阿里云服务器 django+vue前后端分离 nginx配置 centos8下django+vue前后端分离(阿里云)之后端配置展开全文 -
springboot+vue 前后端分离如何整合教程
2019-07-17 14:58:21springboot+vue 前后端分离 整合教程额外补充 详情请查看文档资料 2020年11月12日10:25:51 -
vue前后端分离解决跨域问题
2020-09-18 09:32:58vue前后端分离解决跨域问题vue前后端分离解决跨域问题
参考文章:
(2)https://www.cnblogs.com/xiaoxiaossrs/p/8902535.html
备忘一下。 -
python vue前后端分离_手把手Django+Vue前后端分离开发入门(附demo)
2020-12-08 15:21:08但是,在使用Django的过程中,有一个地方一直是比较难受的,那就是使用Django自带的模版,这种通常需要自己利用HTML+CSS+Jquery的方式总感觉是上一个时代的做法,前后端分离无论对于开发效率、多端支持等等都是很有... -
node+vue前后端分离增删改查
2018-08-17 19:27:39node+vue前后端分离增删改查:node+express+mysql作为后台,vue+bootstrap+jQuery作为前端,简单的增删改查 -
Spring Boot + Vue 前后端分离开发之前端网络请求封装与配置
2020-08-26 00:52:03主要介绍了Spring Boot + Vue 前后端分离开发之前端网络请求封装与配置方法,本文给大家介绍的非常详细,具有一定的参考借鉴价值 ,需要的朋友可以参考下 -
详解springboot和vue前后端分离开发跨域登陆问题
2020-08-25 15:27:40主要介绍了详解springboot和vue前后端分离开发跨域登陆问题,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧 -
SpringBoot+Vue前后端分离实现请求api跨域问题
2020-08-19 01:13:33主要介绍了SpringBoot+Vue前后端分离实现请求api跨域问题,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧 -
Springboot_Vue前后端分离
2020-06-08 12:40:31基于springboot+vue的前后端分离的增删改查,使用了idea+maven+mysql,代码清楚简单,先启动vue项目,再启动springboot就会自动连接前后端。 -
DRF + VUE 前后端分离
2021-02-01 22:53:33DRF + VUE 前后端分离 创建vue项目 先打开终端 打开后输入命令 vue init webpack 项目名字 后端跨域传输数据到前端Vue 后端跨域,先下载 corsheaders 包 python终端输入命令 pip install django-cors-... -
SpringBoot+Vue前后端分离
2020-03-31 17:23:38此项目是基于SpringBoot+Vue的前后端分离开发基础项目,非常简单.使用前记得npm install -
ssm+vue前后端分离框架整合实现(附源码)
2020-10-15 04:00:05主要介绍了ssm+vue前后端分离框架整合实现(附源码),文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧 -
通俗易懂的pringBoot+Vue前后端分离部署教程
2020-03-29 12:57:13最近很火的SpringBoot+Vue前后端分离项目的两种部署方式,适合前后端分离开发,包括前后端一起部署,前后端分离部署 -
SpringBoot+Vue前后端分离,使用SpringSecurity完美处理权限问题的解决方法
2020-08-28 07:47:21主要介绍了SpringBoot+Vue前后端分离,使用SpringSecurity完美处理权限问题,需要的朋友可以参考下 -
Spring Boot+Vue前后端分离开发
2020-07-20 22:03:42我的学习Spring Boot+Vue前后端分离开发笔记 一、Spring Boot+Vue概述 1.前后端分离 是什么? 把一个应用程序的前后端代码分开写。 前端只需要独立编写客户端代码,后端只需要独立编写服务端代码&提供数据接口... -
Vue前后端分离完成文件上传
2020-05-10 16:08:47Vue前后端分离完成文件上传 input: <input type="file" id="imgfile" ref="myfile" style="display: none" onchange="imgfileChange()"> 上传文件 let myfile = this.%refs.myfile; let files = myfile.... -
Nginx+Springboot+Vue 前后端分离 解决跨域问题
2020-08-12 09:05:12Nginx+Springboot+Vue 前后端分离 解决跨域问题 -
Springboot+Vue前后端分离开发常用注解
2019-03-11 20:40:51在我们经历了SSM三框架整合开发后,我们现在技术往Springboot + Vue前后端分离进行开发,在我们进行企业级项目开发时我们会使用各种各样的注解来方便我们的开发速度,通过注解是我们的开发更简单,Springboot+Vue... -
SpringBoot+Vue前后端分离,使用SpringSecurity完美处理
2019-06-26 17:46:17SpringBoot+Vue前后端分离,使用SpringSecurity完美处理系列文章转载 SpringBoot+Vue前后端分离,使用SpringSecurity完美处理 原文地址:https://segmentfault.com/a/1190000012742102 ... -
springboot&&vue前后端分离入门案例
2020-05-08 22:00:53vue前后端分离入门案例 1、最近一直再学习springboot的相关内容,感觉暂时不想学了,就上网随便搜搜,看到大家都在讨论的前后端分离,出于好奇,今天也来玩玩。 2、玩前后端分离,有一个前提条件,后端的也就是... -
SpringBoot+Vue前后端分离的分布式项目实战教程
2020-06-10 11:40:08Web开发发展至今,前后端分离已经成为一种大趋势。 前后端分离会为以后的... 本课程通过一个在线教育为雏形,进行前后端分离的实战,可以让你快速的掌握SpringBoot+Vue进行项目的搭建,应对企业开发和企业面试。 -
Spring Boot + Vue前后端分离(二)前端Vue启动流程
2020-02-01 21:29:16上篇文章为大家讲述了Spring Boot + Vue前后端分离(一)前端Vue环境搭建;本篇文章接着上篇内容继续为大家介绍 前端Vue启动流程。 本文是Spring Boot + Vue前后端分离 系列的第二篇,了解前面的文章有助于更好的... -
搭建spring-boot+vue前后端分离框架并实现登录功能
2018-08-12 12:17:01源码链接:... 一、环境、工具 jdk1.8 maven spring-boot ...vue 二、搭建后台spring-boot框架 步骤: 1、new-project选择SpringInitializr next 2、创建项目文件结构以...