spring boot系列03--spring security (基于数据库)登录和权限控制(上)
这篇打算写一下登陆权限验证相关
说起来也都是泪,之前涉及权限的比较少所以这次准备起来就比较困难。
踩了好几个大坑,还好最终都一一消化掉(这是废话你没解决你写个什么劲 😂) 也补充了一下自己在权限知识的的空白,还是很欣慰的。
试着能把遭遇到的坑都写出来 ,能耐有限 尽力吧
(因为我是基于上一篇的基础上写的 理论上可以拿着上一版的代码直接用 当然我也会在最后放上这次的代码
说是"理论上" 是因为这次遭遇的问题太多反复删,改 很多地方我自己也没有做对比 也记不住了😌)
好了,先从配置开始
配置
向pom.xml文件里追加数据库用和Security用及mybatis用jar 如下
<!--spring security --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-springsecurity4</artifactId> </dependency> <!--postgresql --> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> </dependency> <!--mybatis --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.2.0</version> </dependency>
数据库连接配置和端口号(我自己电脑上还有别的项目在跑避免多端口冲突 配置了端口号 9090)
############### ## DB ############### spring.datasource.url=jdbc:postgresql://localhost:5432/springboot
spring.datasource.username=postgres
spring.datasource.password=postgres
spring.datasource.driver-class-name=org.postgresql.Driver
###############
## Server Port
###############
server.port = 9090
根据自己情况调整,换成其他数据库也是改这两个文件
使用的到的表,主要是用户表和权限表,如下
CREATE TABLE public.mst_authorities ( user_id integer, authority character varying(256), permission_flag character varying(1), create_user character varying(256), create_date_time character varying(256), update_date_time character varying(256), CONSTRAINT authorities_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.mst_users (user_id) MATCH SIMPLE ON UPDATE NO ACTION ON DELETE NO ACTION ); CREATE TABLE public.mst_users ( user_id integer NOT NULL, login_id character varying(256), password character varying(256), display_name character varying(255), enabled boolean, CONSTRAINT users_pkey PRIMARY KEY (user_id) );
下面是我用的数据提供参考 上面是权限下面是用户
画面
先说一下登录(login.html)画面
1 <!DOCTYPE HTML> 2 <!-- thymeleaf 导入 --> 3 <html xmlns:th="http://www.thymeleaf.org" 4 xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4"> 5 <head> 6 <title>登录</title> 7 <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> 8 <script type="text/javascript" src="../js/jquery-3.2.1.min.js" th:src="@{/js/jquery-3.2.1.min.js}"></script> 9 <link rel="stylesheet" href="../css/bootstrap.min.css" th:src="@{/css/bootstrap.min.css}" type="text/css"></link> 10 <script type="text/javascript"> 11 $(function() { 12 }) 13 </script> 14 <body> 15 <!-- common 的 定义好的 header模板 引用 参数 title --> 16 <!-- 17 Bootstrap的容器Class使用 18 container :用于固定宽度并支持响应式布局的容器 19 container-fluid :用于 100% 宽度,占据全部视口(viewport)的容器。 20 --> 21 <div class="container" th:replace="common :: header('login') "></div> 22 <div class="container"> 23 <!--Bootstrap 24 .row:行控制 一行有12列 25 .clo-md-4:列控制 表示 占 4列 26 .md-offfset-4:向右侧偏移4 27 --> 28 <div class="row"> 29 <div class="col-md-4 col-md-offset-4"> 30 <div class="panel panel-default"> 31 <div class="panel-heading"> 32 <h3 class="panel-title">Login</h3> 33 </div> 34 <div class="panel-body"> 35 <form id="frmLogin" method="post" th:action="@{'/login'}" > 36 <div class="form-group form-inline"> 37 <label for="txtUserName" class="col-md-3 control-label" 38 style="text-align: right;">用户名</label> 39 <div class="col-md-9"> 40 <input class="form-control" style="width:60%;" placeholder="请输入用户名 ∗" autofocus="autofocus" name="txtUserCd" type="text" value="" 41 th:id="usrCode" required="required"/> 42 </div> 43 </div> 44 <div class="form-group form-inline" style="padding-top:45px"> 45 <label for="txtUserPwd" class="col-sm-3 control-label" 46 style="text-align: right;">密码</label> 47 <div class="col-md-9"> 48 <input class="form-control" style="width:60%;" placeholder="password ∗" name="txtUserPwd" type="password" value="" required="required" /> 49 </div> 50 </div> 51 <div class="form-group"> 52 <div class="col-md-offset-3 col-md-9"> 53 <div class="checkbox"> 54 <label> <input type="checkbox" />请记住我(未实现) 55 </label> 56 </div> 57 </div> 58 </div> 59 <div class="form-group"> 60 <div class="col-md-offset-3 col-md-5"> 61 <button class="btn btn-primary btn-block" type="submit" name="action" 62 value="login">登录</button> 63 </div> 64 65 </div> 66 <div class="form-group"> 67 <div class=" col-md-12" style="padding-top:15px"> 68 <div class="alert alert-danger" role="alert" th:if="${loginError}"> 69 <strong>用户名或者用户密码不正确,请重新输入</strong> 70 </div> 71 </div> 72 </div> 73 </form> 74 75 </div> 76 </div> 77 </div> 78 </div> 79 </div> 80 81 <!-- common 的 定义好的 fotter模板引用 无参 --> 82 <div class="container" th:replace="common :: footer"></div> 83 </body> 84 </head> 85 </html>
说一下这个画面遭遇的问题吧
坑一、L8,9 的资源引用
spring boot 有自己默认的资源引用文件夹(/META-INF/resources/ ,/resources/ ,/static/ ,/public/)所以用到的资源文件一定要这些
下面否则会读取不到,当然自己也可以自己定义直接写在application.properties文件里就好了,有一个问题就是 如果自己配置了默认的就不能用了
坑二、L40,48
这里的用户名和密码的控件的name是和后台传给
formLogin().usernameParameter("txtUserCd") .passwordParameter("txtUserPwd") 这个两个函数的参数要保持一致
在网上看到说一定要 用 username 和 password 看来也不是必需 但 保持一致 还是有必要的
坑三、这次demo里没有用到但遭遇过 关于ajax 被拦截的问题
参考我这篇 Spring Security Ajax 被拦截
然后说一下登录成功跳转到的main_menu.html画面
1 <!DOCTYPE HTML> 2 <html xmlns:th="http://www.thymeleaf.org" 3 xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4"> 4 <head> 5 <title>MAIN MENU</title> 6 <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> 7 <meta http-equiv="X-UA-Compatible" content="IE=edge" /> 8 <script type="text/javascript" src="../js/jquery-3.2.1.min.js" th:src="@{/js/jquery-3.2.1.min.js}"></script> 9 <link rel="stylesheet" href="../css/bootstrap.min.css" th:src="@{/css/bootstrap.min.css}" type="text/css"></link> 10 11 </head> 12 <body style="margin:0;" > 13 <div class="container-fluid" th:replace="common :: header('MAIN MENU')"></div> 14 <div class="container" style="margin-top:50px;"> 15 <div class="row"> 16 <div class="col-md-offset-2 col-md-8"> 17 <div th:if="${#authorization.expression('hasAnyAuthority(''__${session.authorityKindMap}__'')')}"> 18 当前用户权限:<div th:text="${session.authorityKindMap}">...</div> 19 </div> 20 21 <a href="stow_menu.html" th:href="@{/stow_menu.html}" sec:authorize="hasAnyAuthority('ROLE_USER','ROLE_ADMIN')" 22 class="btn btn-primary btn-lg btn-block" >MENU(普通以上权限)</a> 23 <a href="#" sec:authorize="!hasAnyAuthority('ROLE_USER','ROLE_ADMIN')" 24 class="btn btn-primary btn-lg btn-block" disabled="disabled">AAAAA</a> 25 <hr/> 26 <a href="manage_menu.html" th:href="@{/manage_menu.html}" sec:authorize="hasAnyAuthority('ROLE_ADMIN')" 27 class="btn btn-primary btn-lg btn-block">MENU(管理员以上权限)</a> 28 <a href="#" sec:authorize="!hasAnyAuthority('ROLE_ADMIN')" 29 class="btn btn-primary btn-lg btn-block" disabled="disabled">管理MENU</a> 30 <hr/> 31 </div> 32 </div> 33 </div> 34 <div class="container" th:replace="common :: footer"></div> 35 </body> 36 </html>
spring security 是可以控制到控件的
L17 authorization.expression 先说这个函数 从网上查 然而并没有发现有效资料 没办法只好去翻一下源码了
从这里并看不出什么 只能推测返回的是一个Boolean 和 配合 thymeleaf 的 th:if
继续 看它调用的 函数 AuthUtils.authorizeUsingAccessExpression
源码比较多截取一下关键信息 这里值得注意的就是 L237 这个判断以为都是返回的false
从 L240 和 L249 的log 来分析看 一个是 Access GRANTED(授予访问权限) 一个是Access DENIED(拒绝访问)
到此也可以推出你对你传入表达式的权限判断,如果在看一下L237的代码基本就比较明了
1 public final class ExpressionUtils { 2 3 public static boolean evaluateAsBoolean(Expression expr, EvaluationContext ctx) { 4 try { 5 return ((Boolean) expr.getValue(ctx, Boolean.class)).booleanValue(); 6 } 7 catch (EvaluationException e) { 8 throw new IllegalArgumentException("Failed to evaluate expression '" 9 + expr.getExpressionString() + "'", e); 10 } 11 } 12 }
尤其是 new IllegalArgumentException 这个错误信息 文档里给的描述 [Represent an exception that occurs during expression evaluation.]执行表达式出错
当然这样的说还是有些武断的 但这次不是解读源码 所以能服务于我们这次demo就可以了
然后是还是当前行(L17)的 hasAnyAuthority 这个函数
先说一下 研究它的时候一个小插曲
我查这函数的时候是英文版和中文版对照着看 发现所谓的中文版就是 谷歌出来 贴上了 我也是无力吐槽了 贴下来感受一下
【hasAnyAuthority([authority1,authority2])
如果当前的主体有任何提供的角色(给定的作为一个逗号分隔的字符串列表)的话,返回true
.】
其实这里就是用这个函数来控制像访问这个控件或者画面所需要的权限可以一个也可以是多个
最后再说一个细节也是当前行(L17)
hasAnyAuthority(''__${session.authorityKindMap}__'')
就是这个标红的下划线 意思是预处理 如果不这么写 hasAnyAuthority会报上面说到的这个EvaluationException错 因为直接 被解析字符串了
所以加上预处理把session里的内容提前取出来
然后说加这一行的原因是 下面的都是权限都是硬编码写死的这里写一个从数据库取的动态的
L21,26
sec:authorize 这个就是 spring security的标准标签 没什么要说的
hasAnyAuthority('ROLE_USER','ROLE_ADMIN') 函数是说当前的这个标签 需要 'ROLE_USER','ROLE_ADMIN' 只有拥有这两个权限的任何一个就能访问
然后要说的 这两个参数是取决于你自己定义的是什么 或作说你在后台向这个 Collection<GrantedAuthority> list 放进取的是什么
也是不是网上说必须 以 "ROLE_"开头 也可以是其他的
最后说一下 如果没有权限或者 其他的错误默认被拦截到 error.html
画面本身是没什么可以说的 就是 显示error 或者 提示重新登录
这个 画面名值得说一下 因为 spring security 默认 把一些 错比如 404,403 甚至 空指针 直接映射到这个 叫error.html的画面上
本来想一篇写完呢 写着写着就多了 还有很多重要的没写 都放到一篇里 篇幅有点多了 那就写两篇吧
上 就先到这。。。