springboot框架整合大全(二)
Spring翻译为中文是“春天”,的确,在某段时间内,它给Java开发人员带来过春天,但是随着项目规模的扩大,Spring需要配置的地方就越来越多,夸张点说,“配置两小时,Coding五分钟”。这种纷繁复杂的xml配置随着软件行业一步步地发展,必将逐步退出历史舞台。此时,springboot的出现改变了java的开发,从此变得便利。
Thymeleaf
稍微摘自官网的内容
Thymeleaf is a modern server-side Java template engine for both web and standalone environments.
Thymeleaf's main goal is to bring elegant natural templates to your development workflow — HTML that can be correctly displayed in browsers and also work as static prototypes, allowing for stronger collaboration in development teams.
With modules for Spring Framework, a host of integrations with your favourite tools, and the ability to plug in your own functionality, Thymeleaf is ideal for modern-day HTML5 JVM web development — although there is much more it can do.
机翻后,就是(作者稍作修改):
Thymeleaf是适用于Web和独立环境的现代服务器端Java模板引擎。
Thymeleaf的主要目标是为您的开发工作流程带来优雅的模板,HTML可以在浏览器中正确显示,也可以作为静态原型工作,从而可以在开发团队中加强协作。
Thymeleaf拥有用于Spring Framework的模块,与您喜欢的工具(例如springboot)的大量集成以及嵌入您自己的功能的能力,对于现代HTML5 JVM Web开发而言,Thymeleaf是理想的选择-尽管它还有很多工作要做。
不得不说Thymeleaf和JSP十分得像,但是他们之间的区别在于,不运行项目之前,Thymeleaf也是纯HTML(不需要服务端的支持)而JSP需要进行一定的转换,这样就方便前端人员进行独立的设计、调试。它有如下三个吸引人的特点:
Thymeleaf 在有网络和无网络的环境下皆可运行,即它可以让美工在浏览器查看页面的静态效果,也可以让程序员在服务器查看带数据的动态页面效果。这是由于它支持 html 原型,然后在 html 标签里增加额外的属性来达到模板+数据的展示方式。浏览器解释 html 时会忽略未定义的标签属性,所以 thymeleaf 的模板可以静态地运行;当有数据返回到页面时,Thymeleaf 标签会动态地替换掉静态内容,使页面动态显示。
Thymeleaf 开箱即用的特性。它提供标准和spring标准两种方言,可以直接套用模板实现JSTL、 OGNL表达式效果,避免每天套模板、该jstl、改标签的困扰。同时开发人员也可以扩展和创建自定义的方言。
Thymeleaf 提供spring标准方言和一个与 SpringMVC 完美集成的可选模块,可以快速的实现表单绑定、属性编辑器、国际化等功能。
pom依赖引入
<!-- springboot thymeleaf starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
html引入
在html页面中引入:
<!DOCTYPE html>
<html xmlns:th=“http://www.thymeleaf.org”>
...
</html>
准备工作基本完毕,注意:xmlns:th中th,个人认为是一个命名空间,在使用时需要引用,强烈建议不改,笔者查阅了大量的资料,几乎都是用的th
语法规则
Thymeleaf 作为一种模板引擎,它拥有自己的语法规则。Thymeleaf 语法分为以下 2 类:
标准表达式语法(Standard Expression Syntax)
th 属性
标准表达式语法
Thymeleaf 模板引擎支持多种表达式:
变量表达式:
${变量名称}
,该表达式具有以下功能:获取对象的属性和方法
<!-- 获取 person 对象的 lastName 属性 --> ${person.lastName}
使用内置的基本对象,Thymeleaf 中常用的内置基本对象如下:
#ctx :上下文对象;
#vars :上下文变量;
#locale:上下文的语言环境;
#request:HttpServletRequest 对象(仅在 Web 应用中可用);
#response:HttpServletResponse 对象(仅在 Web 应用中可用);
#session:HttpSession 对象(仅在 Web 应用中可用);
#servletContext:ServletContext 对象(仅在 Web 应用中可用)。
<!-- 获取到 session 对象中的 map 属性 --> ${#session.getAttribute('map')} ${session.map}
使用内置的工具对象,除了能使用内置的基本对象外,变量表达式还可以使用一些内置的工具对象。
strings:字符串工具对象,常用方法有:equals、equalsIgnoreCase、length、trim、toUpperCase、toLowerCase、indexOf、substring、replace、startsWith、endsWith,contains 和 containsIgnoreCase 等;
numbers:数字工具对象,常用的方法有:formatDecimal 等;
bools:布尔工具对象,常用的方法有:isTrue 和 isFalse 等;
arrays:数组工具对象,常用的方法有:toArray、length、isEmpty、contains 和 containsAll 等;
lists/sets:List/Set 集合工具对象,常用的方法有:toList、size、isEmpty、contains、containsAll 和 sort 等;
maps:Map 集合工具对象,常用的方法有:size、isEmpty、containsKey 和 containsValue 等;
dates:日期工具对象,常用的方法有:format、year、month、hour 和 createNow 等。
<!-- 使用内置工具对象 strings,判断字符串与对象的某个属性是否相等 --> ${#strings.equals('jhon',name)}
选择变量表达式:
*{...}
,选择变量表达式与变量表达式功能基本一致,只是在变量表达式的基础上增加了与th:object
的配合使用。当使用th:object
存储一个对象后,我们可以在其后代中使用选择变量表达式(*{...}
)获取该对象中的属性,其中,*
即代表该对象。
<!-- th:object 用于存储一个临时变量,该变量只在该标签及其后代中有效 -->
<div th:object="${session.user}" >
<p th:text="*{fisrtName}">firstname</p>
</div>
链接表达式:
@{/某页面}
,链接表达式的形式结构如下:无参请求:
@{/xxx}
有参请求:
@{/xxx(k1=v1,k2=v2)}
<link href="asserts/css/signin.css" th:href="@{/asserts/css/signin.css}" rel="stylesheet">
国际化表达式:
#{msg}
,消息表达式一般用于国际化的场景。结构如下。
th:text="#{msg}"
片段引用表达式:
~{...}
,片段引用表达式用于在模板页面中引用其他的模板片段,该表达式支持以下两中语法结构:推荐:
~{templatename::fragmentname}
支持:
~{templatename::#id}
以上语法结构说明如下:
templatename
:模版名,Thymeleaf 会根据模版名解析完整路径:/resources/templates/templatename.html,要注意文件的路径。fragmentname
:片段名,Thymeleaf 通过th:fragment
声明定义代码块,即:th:fragment="fragmentname"
id
:HTML 的 id 选择器,使用时要在前面加上 # 号,不支持 class 选择器。
th 属性
Thymeleaf 还提供了大量的 th 属性,这些属性可以直接在 HTML 标签中使用,其中常用 th 属性及其示例如下:
th:id
:替换 HTML 的 id 属性<input id="html-id" th:id="thymeleaf-id" />
th:text
:文本替换,转义特殊字符<h1 th:text="hello,bianchengbang" >hello</h1>
th:utext
:文本替换,不转义特殊字符<div th:utext="'<h1>hello</h1>'">欢迎你</div>
th:object
:在父标签选择对象,子标签使用 *{…} 选择表达式选取值。 没有选择对象,那子标签使用选择表达式和 ${…} 变量表达式是一样的效果。 同时即使选择了对象,子标签仍然可以使用变量表达式。<div th:object="${session.user}" > <p th:text="*{fisrtName}">firstname</p> </div>
th:value
:替换 value 属性<input th:value = "${user.name}" />
th:with
:局部变量赋值运算<div th:with="isEvens = ${prodStat.count}%2 == 0" th:text="${isEvens}"></div>
th:style
:设置样式<div th:style="'color:#F00; font-weight:bold'">HelloWorld</div>
th:onclick
:点击事件<td th:onclick = "'getInfo()'"></td>
th:each
:遍历,支持 Iterable、Map、数组等。<table> <tr th:each="m:${session.map}"> <td th:text="${m.getKey()}"></td> <td th:text="${m.getValue()}"></td> </tr> </table>`
th:if
:根据条件判断是否需要展示此标签<a th:if ="${userId == collect.userId}">
th:unless
:和 th:if 判断相反,满足条件时不显示<div th:unless="${m.getKey()=='name'}" ></div>
th:switch
:与 Java 的 switch case语句类似,通常与 th:case 配合使用,根据不同的条件展示不同的内容<div th:switch="${name}"> <span th:case="a">你好</span> <span th:case="b">HelloWorld</span> </div>
th:fragment
:模板布局,类似 JSP 的 tag,用来定义一段被引用或包含的模板片段<footer th:fragment="footer">插入的内容</footer>
th:insert
:布局标签; 将使用 th:fragment 属性指定的模板片段(包含标签)插入到当前标签中。<div th:insert="commons/bar::footer"></div>
th:replace
:布局标签; 使用 th:fragment 属性指定的模板片段(包含标签)替换当前整个标签。<div th:replace="commons/bar::footer"></div>
th:selected
:select 选择框选中<select> <option>---</option> <option th:selected="${name=='a'}">你好吗</option> <option th:selected="${name=='b'}">HelloWorld</option> </select>
th:src
:替换 HTML 中的 src 属性<img th:src="@{/asserts/img/bootstrap-solid.svg}" src="asserts/img/bootstrap-solid.svg" />
th:inline
:内联属性; 该属性有 text、none、javascript 三种取值, 在<script>
标签中使用时,js 代码中可以获取到后台传递页面的对象。<script type="text/javascript" th:inline="javascript"> var name = /*[[${name}]]*/'bianchengbang'; alert(name) </script>`
th:action
:替换表单提交地址<form th:action="@{/user/login}" th:method="post"></form>
公共页面抽取
和html中的iframe一样,Thymeleaf 允许你将公共的页面部分抽取到独立的模板中,这样可以避免重复代码,提高代码可维护性。
首先抽离出一个独立的公共页面片段,保存在commons.html
<div th:fragment="fragment-name" id="fragment-id">
<span>公共页面片段</span>
</div>
引用公共页面
可以使用以下指令
th:replace:把独立的模板替换为主模板中的内容。
th:insert:把独立的模板插入为主模板中的一部分。
th:include:把独立的模板引入为主模板中的内容。
以上指令中的值可以为如下:
~{templatename::selector}
:模板名::选择器~{templatename::fragmentname}
:模板名::片段名
通常情况下,~{}
可以省略,其行内写法为[[~{...}]]
或[(~{...})]
,其中[[~{...}]]
会转义特殊字符,[(~{...})]
则不会转义特殊字符。
在页面 fragment.html 中引入 commons.html 中声明的页面片段,可以通过以下方式实现。
<!--th:insert 片段名引入-->
<div th:insert="commons::fragment-name"></div>
<!--th:insert id 选择器引入-->
<div th:insert="commons::#fragment-id"></div>
------------------------------------------------
<!--th:replace 片段名引入-->
<div th:replace="commons::fragment-name"></div>
<!--th:replace id 选择器引入-->
<div th:replace="commons::#fragment-id"></div>
------------------------------------------------
<!--th:include 片段名引入-->
<div th:include="commons::fragment-name"></div>
<!--th:include id 选择器引入-->
<div th:include="commons::#fragment-id"></div>
传递参数
Thymeleaf 在抽取和引入公共页面片段时,还可以进行参数传递。引用公共页面片段时,我们可以通过以下 2 种方式,将参数传入到被引用的页面片段中:
模板名::选择器名或片段名(参数1=参数值1,参数2=参数值2)
模板名::选择器名或片段名(参数值1,参数值2)
注:
若传入参数较少时,一般采用第二种方式,直接将参数值传入页面片段中;
若参数较多时,建议使用第一种方式,明确指定参数名和参数值。
示例代码如下:
<!--th:insert 片段名引入-->
<div th:insert="commons::fragment-name(var1='insert-name',var2='insert-name2')"></div>
<!--th:insert id 选择器引入-->
<div th:insert="commons::#fragment-id(var1='insert-id',var2='insert-id2')"></div>
------------------------------------------------
<!--th:replace 片段名引入-->
<div th:replace="commons::fragment-name(var1='replace-name',var2='replace-name2')"></div>
<!--th:replace id 选择器引入-->
<div th:replace="commons::#fragment-id(var1='replace-id',var2='replace-id2')"></div>
------------------------------------------------
<!--th:include 片段名引入-->
<div th:include="commons::fragment-name(var1='include-name',var2='include-name2')"></div>
<!--th:include id 选择器引入-->
<div th:include="commons::#fragment-id(var1='include-id',var2='include-id2')"></div>
在声明页面片段时,我们可以在片段中声明并使用这些参数,例如:
<!--使用 var1 和 var2 声明传入的参数,并在该片段中直接使用这些参数 -->
<div th:fragment="fragment-name(var1,var2)" id="fragment-id">
<p th:text="'参数1:'+${var1} + '-------------------参数2:' + ${var2}">...</p>
</div>
thymeleaf使用实例
对象遍历
后端首先发送一个对象
import org.springframework.web.bind.annotation.RequestMapping;
@RequestMapping("/pageAction")
public String pageAction(Model model){
List<user> list = Service.queryList();
model.addAttribute("list",list);
return "page";
}
user对象按照如下方式建立,注意生成getter和setter方法。Serializable是将对象序列化,建议搞上去。
import java.io.Serializable;
public class user implements Serializable {
private int id;
private String name;
}
前端循环遍历对象:
<div th:each="list : ${list}">
展示,注意要在遍历对象的盒子里边:
<div th:each="list : ${list}">
<span th:text="${list.name}"></span> <!-- 展示在页面上使用text -->
<input type="text" th:value="${list.name}" /><!-- 嵌套在input或其他用value -->
</div>
可以使用#numbers.sequence()
限制展示的数量
<li th:each="index:${#numbers.sequence(1,10)}">
<a th:text="${index}">1</a><!-- 这里会展示1->10 -->
</li>
超链接
<a th:href="@{/pageAction(id = ${list.id},name = ${list.name})}">1</a>
若点击1,url上会显示
localhost:8080/pageAction?id=xxx&name=xxx
Shiro
Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的API可以快速、轻松地获得任何应用程序,不论是从最小的移动应用程序到最大的网络和企业应用程序。
shiro有三个核心的组件,分别是:Subject, SecurityManager 和 Realms.
Subject:即“当前操作用户”。但是,在Shiro中,Subject这一概念并不仅仅指人,也可以是第三方进程、后台帐户(Daemon Account)或其他类似事物。它仅仅意味着"当前跟软件交互的东西"。Subject代表了当前用户的安全操作,SecurityManager则管理所有用户的安全操作。
SecurityManager:它是Shiro框架的核心,典型的Facade模式,Shiro通过SecurityManager来管理内部组件实例,并通过它来提供安全管理的各种服务。
Realm: Realm充当了Shiro与应用安全数据间的“桥梁”或者“连接器”。也就是说,当对用户执行认证(登录)和授权(访问控制)验证时,Shiro会从应用配置的Realm中查找用户及其权限信息。
shiro的大体框架为:
使用springboot也可以将其整合在里面,让自己的系统更加安全强大
引入依赖
<!-- shiro的核心包 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.4.1</version>
</dependency>
<!-- log4j -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
QuickStart
在进入学习之前,官方给出了一个快速开始的项目demo,详情如下
以下是QuickStart的全部代码,这里笔者自己注释了一下,分解可以看到有6个部分,大致是SecurityManagerFactory
(安全管理的工厂,属于工厂模式)、SecurityManager
(安全管理,三大核心之一)、Subject
(对象,三大核心之一)、Authenticated
(认证)、Role
(角色)、Permitted
(授权)
package com.example.quickstart;
import org.apache.log4j.Logger;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Factory;
public class QuickStart {
private static Logger log = Logger.getLogger(QuickStart.class);
public static void main(String[] args) {
//1.创建shiro工厂
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
//2.创建SecurityManager
SecurityManager securityManager = factory.getInstance();
//3.SecurityUtils设置SecurityManager
SecurityUtils.setSecurityManager(securityManager);
//4.获取Subject对象 *
Subject currentUser = SecurityUtils.getSubject();
//5.创建shiro的session
Session session = currentUser.getSession();
session.setAttribute("someKey", "aValue");//设置session属性
String value = (String) session.getAttribute("someKey");
if (value.equals("aValue")) {
log.info("Retrieved the correct value! [" + value + "]");
}
//判断该用户是否被认证
if (!currentUser.isAuthenticated()) {
UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
token.setRememberMe(true);
try {
currentUser.login(token);
} catch (UnknownAccountException uae) {
log.info("There is no user with username of " + token.getPrincipal());
} catch (IncorrectCredentialsException ice) {
log.info("Password for account " + token.getPrincipal() + " was incorrect!");
} catch (LockedAccountException lae) {
log.info("The account for username " + token.getPrincipal() + " is locked. " +
"Please contact your administrator to unlock it.");
}
// ... catch more exceptions here (maybe custom ones specific to your application?
catch (AuthenticationException ae) {
//unexpected condition? error?
}
}
//say who they are:
//print their identifying principal (in this case, a username):
log.info("User [" + currentUser.getPrincipal() + "] logged in successfully.");
//test a role:角色
if (currentUser.hasRole("schwartz")) {
log.info("May the Schwartz be with you!");
} else {
log.info("Hello, mere mortal.");
}
//test a typed permission (not instance-level)粗粒度
if (currentUser.isPermitted("lightsaber:wield")) {
log.info("You may use a lightsaber ring. Use it wisely.");
} else {
log.info("Sorry, lightsaber rings are for schwartz masters only.");
}
//a (very powerful) Instance Level permission:细粒度
if (currentUser.isPermitted("winnebago:drive:eagle5")) {
log.info("You are permitted to 'drive' the winnebago with license plate (id) 'eagle5'. " +
"Here are the keys - have fun!");
} else {
log.info("Sorry, you aren't allowed to drive the 'eagle5' winnebago!");
}
//all done - log out!
currentUser.logout();
}
}
其他配置文件
shiro.ini:
# =============================================================================
# Tutorial INI configuration
#
# Usernames/passwords are based on the classic Mel Brooks' film "Spaceballs" :)
# =============================================================================
# -----------------------------------------------------------------------------
# Users and their (optional) assigned roles
# username = password, role1, role2, ..., roleN
# -----------------------------------------------------------------------------
[users]
root = secret, admin
guest = guest, guest
presidentskroob = 12345, president
darkhelmet = ludicrousspeed, darklord, schwartz
lonestarr = vespa, goodguy, schwartz
# -----------------------------------------------------------------------------
# Roles with assigned permissions
# roleName = perm1, perm2, ..., permN
# -----------------------------------------------------------------------------
[roles]
admin = *
schwartz = lightsaber:*
goodguy = winnebago:drive:eagle5
log4j.properties:
### 设置###
log4j.rootLogger = debug,stdout,D,E
### 输出信息到控制抬 ###
log4j.appender.stdout = org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target = System.out
log4j.appender.stdout.layout = org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern = [%-5p] %d{yyyy-MM-dd HH:mm:ss,SSS} method:%l%n%m%n
### 输出DEBUG 级别以上的日志到=E://logs/error.log ###
log4j.appender.D = org.apache.log4j.DailyRollingFileAppender
#log4j.appender.D.File = E://logs/log.log
log4j.appender.D.Append = true
log4j.appender.D.Threshold = DEBUG
log4j.appender.D.layout = org.apache.log4j.PatternLayout
log4j.appender.D.layout.ConversionPattern = %-d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] - [ %p ] %m%n
### 输出ERROR 级别以上的日志到=E://logs/error.log ###
log4j.appender.E = org.apache.log4j.DailyRollingFileAppender
#log4j.appender.E.File =E://logs/error.log
log4j.appender.E.Append = true
log4j.appender.E.Threshold = ERROR
log4j.appender.E.layout = org.apache.log4j.PatternLayout
log4j.appender.E.layout.ConversionPattern = %-d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] - [ %p ] %m%n
配置类和realm
在springboot中自定义shiro的配置类ShiroConfig
,并在方法前添加@Configuration
(标准的springboot配置类注解),config类中所有的方法前都应该加上@Bean,让spring托管方法
首先引入realm
,这里的自定义realm
命名为UserRealm
,创建一个UserRealm
类,并继承AuthorizingRealm
类:
package com.example.config;
import org.apache.shiro.realm.AuthorizingRealm;
public class UserRealm extends AuthorizingRealm {
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("执行了 => 授权方法doGetAuthorizationInfo");
return null;
}
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("执行了 => 认证方法doGetAuthenticationInfo");
return null;
}
}
在ShiroConfig
类添加方法:
//创建realm对象,需要自定义
@Bean
public UserRealm getUserRealm(){
return new UserRealm();
}
其次引入SecurityManager
,这里使用DefaultWebSecurityManager
,形参中用@Qualifier
指定realm
方法引入UserRealm
:
//DefaultWebSecurityManager-->获取UserRealm
@Bean
public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("getUserRealm") UserRealm userRealm){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(userRealm);//关联realm
return securityManager;
}
最后加入Factory
,这里用ShiroFilterFactoryBean
,获取DefaultWebSecurityManager
的方法同上:
//ShiroFilterFactoryBean-->获取DefaultWebSecurityManager
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("getDefaultWebSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
// 设置安全管理器
bean.setSecurityManager(defaultWebSecurityManager);
return bean;
}
三个方法在config类中是串起来的,顺序是UserRealm
-->DefaultWebSecurityManager
-->ShiroFilterFactoryBean
添加测试页面
添加主页index,两个页面add,update和登录页面login.html
index.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>Index</h1>
<span th:text="${msg}"></span>
<hr>
<a th:href="@{/user/add}">add</a> | <a th:href="@{/user/update}">update</a>
</body>
</html>
login.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/loginCheck">
请输入账号: <input type="text" name="username">
请输入密码: <input type="password" name="password">
<input type="submit">
</form>
</body>
</html>
add.html和update.html添加1级标题即可
Controller编写
编写controller对其进行测试
@RequestMapping("/")
public String index(Model model){
model.addAttribute("msg","hello shiro");
return "index";
}
@RequestMapping("/user/update")
public String update(){
return "update";
}
@RequestMapping("/user/add")
public String add(){
return "add";
}
@RequestMapping("/login")
public String login(){
return "login";
}
登录拦截配置
该操作由shiro的内置过滤器完成,有5大选项:
anon:无需认证可以访问
authc: 必须认证了才能访问
user:必须拥有【记住我】功能才能用
perms: 拥有对某个资源的权限才能访问
role: 拥有某个角色权限才能访问
在ShiroConfig
类的ShiroFilterFactoryBean
操作方法中引入过滤器,完整的方法如下:
//ShiroFilterFactoryBean-->获取DefaultWebSecurityManager
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("getDefaultWebSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
// 设置安全管理器
bean.setSecurityManager(defaultWebSecurityManager);
// 添加shiro的内置过滤器
/*
anon:无需认证可以访问
authc: 必须认证了才能访问
user:必须拥有 记住我 功能才能用
perms: 拥有对某个资源的权限才能访问
role: 拥有某个角色权限才能访问
*/
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
filterChainDefinitionMap.put("/user/*","authc");
bean.setFilterChainDefinitionMap(filterChainDefinitionMap);
//设置登录请求
bean.setLoginUrl("/login");
return bean;
}
可以看到,/user/下面的请求全部被拦截,必须认证了才能访问。在没有认证的时候,会跳转至/login。
认证
登录其实就是一个认证的过程,在controller中添加/loginCheck的操作逻辑
@RequestMapping("/loginCheck")
public String loginCheck(String username,String password){
System.out.println("获取账号密码:username: "+username+",password: "+password);
Subject subject = SecurityUtils.getSubject();//获取当前用户
UsernamePasswordToken token = new UsernamePasswordToken(username,password);//封装登录数据
System.out.println("验证登录");
try {
subject.login(token);//验证登录
} catch (UnknownAccountException uae) {
System.out.println("用户名错误");
} catch (IncorrectCredentialsException ice) {
System.out.println("密码错误");
} catch (LockedAccountException lae) {
System.out.println("账号被锁定");
}
return "index";
}
在执行subject.login(token);
方法时,会将token输送到UserRealm
中的doGetAuthenticationInfo()
方法,验证是否正确,这里需要用户自行配置
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("执行了 => 认证方法doGetAuthenticationInfo");
//从数据库获取账号密码
User user = new User();
String username = "root";
user.setUsername(username);
String password = "1234";
user.setPassword(password);
String perms = "user:add";
user.setPerms(perms);
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
if(!token.getUsername().equals(username)){
return null;//返回错误 UnknownAccountException
}
return new SimpleAuthenticationInfo(user,user.getPassword(),"");//principal放存放着账户对象:USER
}
授权
每个账户都有相应的权限,某些功能不能给某些没有该权限的人使用。首先限制url的权限,在过滤器中添加即可
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
// 设置安全管理器
bean.setSecurityManager(defaultWebSecurityManager);
// 添加shiro的内置过滤器
/*
anon:无需认证可以访问
authc: 必须认证了才能访问
user:必须拥有 记住我 功能才能用
perms: 拥有对某个资源的权限才能访问
role: 拥有某个角色权限才能访问
*/
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
/* 先检测授权后检测拦截,越在上面的优先度越大,未经授权跳转到指定页面 */
filterChainDefinitionMap.put("/user/add","perms[user:add]");
filterChainDefinitionMap.put("/user/update","perms[user:update]");
filterChainDefinitionMap.put("/user/*","authc");
bean.setFilterChainDefinitionMap(filterChainDefinitionMap);
//设置登录请求
bean.setLoginUrl("/login");
//未授权页面
bean.setUnauthorizedUrl("/unau");
return bean;
一定要注意授权和拦截的顺序,并且授权是有优先度的。可以看到未授权也会跳转到相应的url,在controller添加即可:
@RequestMapping("/unau")
@ResponseBody
public String unauthorized(){
return "未经授权无法访问";
}
最后在UserRealm中添加授权逻辑
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("执行了 => 授权方法doGetAuthorizationInfo");
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
// info.addStringPermission("user:add");//为账户添加user:add权限
System.out.println("开始授权");
//拿到登录对象
Subject subject = SecurityUtils.getSubject();
User currentUser = (User) subject.getPrincipal();//拿到User对象
//设置当前用户的权限
info.addStringPermission(currentUser.getPerms());
return info;
}
总结
这里会列举上诉所有代码,仅供参考
ShiroConfig.java
package com.example.config;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.LinkedHashMap;
import java.util.Map;
@Configuration
public class ShiroConfig {
/*
三大核心
realm
subject
SecurityManager
*/
//创建realm对象,需要自定义
@Bean
public UserRealm getUserRealm(){
return new UserRealm();
}
//DefaultWebSecurityManager-->获取UserRealm
@Bean
public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("getUserRealm") UserRealm userRealm){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(userRealm);//关联realm
return securityManager;
}
//ShiroFilterFactoryBean-->获取DefaultWebSecurityManager
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("getDefaultWebSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
// 设置安全管理器
bean.setSecurityManager(defaultWebSecurityManager);
// 添加shiro的内置过滤器
/*
anon:无需认证可以访问
authc: 必须认证了才能访问
user:必须拥有 记住我 功能才能用
perms: 拥有对某个资源的权限才能访问
role: 拥有某个角色权限才能访问
*/
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
/* 先检测授权后检测拦截,越在上面的优先度越大,未经授权跳转到指定页面 */
filterChainDefinitionMap.put("/user/add","perms[user:add]");
filterChainDefinitionMap.put("/user/update","perms[user:update]");
filterChainDefinitionMap.put("/user/*","authc");
bean.setFilterChainDefinitionMap(filterChainDefinitionMap);
//设置登录请求
bean.setLoginUrl("/login");
//未授权页面
bean.setUnauthorizedUrl("/unau");
return bean;
}
}
UserRealm.java
package com.example.config;
import com.example.pojo.User;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.Subject;
public class UserRealm extends AuthorizingRealm {
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("执行了 => 授权方法doGetAuthorizationInfo");
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
// info.addStringPermission("user:add");//为账户添加user:add权限
System.out.println("开始授权");
//拿到登录对象
Subject subject = SecurityUtils.getSubject();
User currentUser = (User) subject.getPrincipal();//拿到User对象
//设置当前用户的权限
info.addStringPermission(currentUser.getPerms());
return info;
}
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("执行了 => 认证方法doGetAuthenticationInfo");
//从数据库获取账号密码
User user = new User();
String username = "root";
user.setUsername(username);
String password = "1234";
user.setPassword(password);
String perms = "user:add";
user.setPerms(perms);
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
if(!token.getUsername().equals(username)){
return null;//返回错误 UnknownAccountException
}
return new SimpleAuthenticationInfo(user,user.getPassword(),"");//principal放存放着账户对象:USER
}
}
exampleController.java
package com.example.controller;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class exampleController {
@RequestMapping("/")
public String index(Model model){
model.addAttribute("msg","hello shiro");
return "index";
}
@RequestMapping("/user/update")
public String update(){
return "update";
}
@RequestMapping("/user/add")
public String add(){
return "add";
}
@RequestMapping("/login")
public String login(){
return "login";
}
}
Dubbo与Zookeeper
dubbo文档:Dubbo 2.7 | Apache Dubbo
zookeeper文档:ZooKeeper 介绍 — zookeeper入门 文档
在进入学习之前,首先要了解一个理论
分布式系统
在《分布式系统原理与范型》一书中有如下定义:“分布式系统是若干独立计算机的集合,这些计算机对于用户来说就像单个相关系统”;
分布式系统是由一组通过网络进行通信、为了完成共同的任务而协调工作的计算机节点组成的系统。分布式系统的出现是为了用廉价的、普通的机器完成单个计算机无法完成的计算、存储任务。其目的是利用更多的机器,处理更多的数据。
分布式系统(distributed system)是建立在网络之上的软件系统。
分布式系统对于用户而言,他们面对的就是一个服务器,提供用户需要的服务而已,而实际上这些服务是通过背后的众多服务器组成的一个分布式系统,因此分布式系统看起来像是一个超级计算机一样。
首先需要明确的是,只有当单个节点的处理能力无法满足日益增长的计算、存储任务的时候,且硬件的提升(加内存、加磁盘、使用更好的CPU)高昂到得不偿失的时候,应用程序也不能进一步优化的时候,我们才需要考虑分布式系统。因为,分布式系统要解决的问题本身就是和单机系统一样的,而由于分布式系统多节点、通过网络通信的拓扑结构,会引入很多单机系统没有的问题,为了解决这些问题又会引入更多的机制、协议,带来更多的问题。
RPC
RPC(Remote Procedure Call)远程过程调用协议,一种通过网络从远程计算机上请求服务,而不需要了解底层网络技术的协议。RPC它假定某些协议的存在,例如TPC/UDP等,为通信程序之间携带信息数据。在OSI网络七层模型中,RPC跨越了传输层和应用层,RPC使得开发,包括网络分布式多程序在内的应用程序更加容易。
过程是什么? 过程就是业务处理、计算任务,更直白的说,就是程序,就是想调用本地方法一样调用远程的过程
如何理解Dubbo与Zookeeper
淘宝每天有超过100万的用户(当然笔者不知道完整的数字),每天都能产生百亿级别的数据,用一个服务器是绝对处理不过来的,真的很难想象如果用一个服务器来处理淘宝用户产生的这些数据,一个用户请求一次需要多长时间,这个时候就需要多个服务器对这些数据进行处理。就好比做作业,如果有一、两份作业,一个人可以做得过来,但是有十份、一百份甚至一千份,就需要更多的人一起做这些作业才能在短时间内完成作业。
但是做作业,如果一个人只做一份,另外一个人做十份,这显然也不合理。
上面的这些思考,属于负载均衡,不同的人做作业,属于分布式
Dubbo是Alibaba开源的分布式服务框架,它最大的特点是按照分层的方式来架构,使用这种方式可以使各个层之间解耦合(或者最大限度地松耦合)。从服务模型的角度来看,Dubbo采用的是一种非常简单的模型,要么是提供方提供服务,要么是消费方消费服务,所以基于这一点可以抽象出生产者(Provider)和消费者(Consumer)两个角色
生产者(Provider),通俗理解来说就是从数据库拿出数据的那个服务,产生数据的一方
消费者(Consumer),通俗理解来说就是拿到数据后,用这些数据搞事情的那个服务,消费数据的一方
zookeeper用来注册服务和进行负载均衡,哪一个服务由哪一个机器来提供必需让调用者知道。
所以dubbo作为接口的RPC服务框架。而zookeeper就作为将这些服务管理起来,进行科学分配作业的框架,也叫zookeeper集群。
Zookeeper的安装
zookeeper下载地址为:Apache ZooKeeper
首先进入页面,点击一个稳定的版本,进入后下载压缩包
注意,一定要下载带-bin
这个后缀的版本,这个是编译后的压缩包,否则会报Could not find or load main class org.apache.zookeeper.server.quorum.QuorumPeerMain
的错误
Windows
下载完毕后,解压下来
将conf
目录下的zoo_sample.cfg
文件,复制一份,重命名为zoo.cfg
在安装目录下面新建一个空的data
文件夹和log
文件夹
修改zoo.cfg
配置文件,将dataDir=/tmp/zookeeper
修改成 zookeeper 安装目录所在的 data 文件夹,再添加一条添加数据日志的配置(可以根据自己的目录进行修改),以下为作者的配置:
dataDir=D:/apache-zookeeper-3.7.1-bin/data
dataLogDir=D:/apache-zookeeper-3.7.1-bin/log
从clientPort=2181
可看到zookeeper的默认端口为2181,当然也可以修改
配置完毕后,双击/bin/zkServer.cmd
启动zookeeper服务端的服务(或者以管理员身份运行)
控制台显示 bind to port 0.0.0.0/0.0.0.0:2181,表示服务端启动成功
双击/bin/zkCli.cmd
启动zookeeper客户端的服务(或者以管理员身份运行),出现 Welcome to Zookeeper!,表示成功启动客户端。
Linux
参照windows,客户端为zkCli.sh
和服务端zkServer.sh
Dubbo-admin的安装与使用
下载地址:GitHub - apache/dubbo-admin: The ops and reference implementation for Apache Dubbo
下载后,解压得到如下文件夹
进入dubbo-admin-server
,按路径src
、main
、resources
找到application.properties
,编辑该文件。推荐用idea或者eclipse这些ide软件打开这些项目
添加server.port=7001
,为这个项目修改端口,否则默认8080端口启动。注意admin.registry.address
、admin.root.user.name=root
和admin.root.user.password=root
,它们分别是注册地址(zookeeper)、dubbo-admin的账号密码,配置后结果如下:
使用mvn clean
清理整个项目
上述步骤完毕后,启动zookeeper服务,然后找到DubboAdminApplication.java
,启动它
由于笔者下载的是最新版的dubbo-admin,它是个前后端分离项目(所谓前后端分离,实际上就是前端的项目和后端的项目独立开来,物理上,前端要开一个服务,后端也要开一个服务),后端用springboot,前端用的是vue,所以需要下载一个npm。具体的教程后面会出,或者直接百度吧。
进入dubbo-admin-ui
,编辑vue.config.js
,将第33行中的target: 'http://localhost:8080/'
修改成dubbo-admin-server
中设置的server.port
也就是7001,当然这个也是可以自由修改的
打开终端,进入到dubbo-admin-ui
后,进行npm install
安装相应的包,安装完毕后,使用npm run dev
打开dubbo-admin前端
打开浏览器,输入localhost:8082
进入登录页面(这个也可以修改,在vue.config.js
的第24行),根据上面dubbo-admin-server
中application.properties
的admin.root.user.name
和admin.root.user.password
设置输入登录账号和密码(笔者方便测试默认都是root),输入完毕后点击登录
见到如下页面,dubbo-admin准备完成
生产者(provider)的搭建
搭建之前,首先打开zookeeper服务端、dubbo-admin,并创建好数据库
引入依赖--dubbo
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>2.7.3</version>
</dependency>
引入依赖--zookeeper
引入zkclient
,zookeeper客户端
<!--zkclient-->
<dependency>
<groupId>com.github.sgroschupf</groupId>
<artifactId>zkclient</artifactId>
<version>0.1</version>
</dependency>
引入zookeeper
,但是需要解决日志冲突问题(新版zookeeper的坑)
<!--引入zookeeper并解决日志冲突-->
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.14</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
</exclusions>
</dependency>
解决java.lang.NoClassDefFoundError: org/apache/curator/framework/recipes/cache/TreeCacheListener
问题
<!--解决 java.lang.NoClassDefFoundError: org/apache/curator/framework/recipes/cache/TreeCacheListener-->
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>2.12.0</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>2.8.0</version>
</dependency>
引入依赖--其他
spring-boot-starter-web
spring-boot-starter-jdbc
Druid
Mybatis
mysql-connector-java
具体的引入参考数据库资源管理这一节
搭建Service
该章节的操作步骤和数据库资源管理这一节一样,但是注意一个点,实现类中,@Service
的包为import org.apache.dubbo.config.annotation.Service;
开了这个,才能让dubbo服务被发现
注意在bean类implements
Serializable
,让bean序列化
搭建完毕后,该服务放在后台即可
配置编写
参考如下配置
dubbo:
# 注册服务应用名字
application:
name: provider-server
# 注册中心地址
registry:
address: zookeeper://127.0.0.1:2181
# 哪些服务需要被注册,扫描的包为注释了@Service的类
scan:
base-packages: com.example.providerserver.implement
# 随机dubbo端口号,否则可能存在端口冲突的问题
protocol:
port: -1
消费者(consumer)的搭建
引入依赖
dubbo+zookeeper:与生产者的搭建一样
其他:spring-boot-starter-web
Service地址映射
provider中的Service的包是com.example.providerserver.service.BigdatatestService
,bean的包是com.example.providerserver.pojo.bigdatatest
在consumer中也要新建一个相同包名,相同类名的service和pojo,用于映射生产者的service。bean类同样也要implements
Serializable
,让bean序列化
这就是为什么consumer在引入依赖不需要mybatis这些数据库操作框架的原因,完成后消费者结构如下:
新建controller,映射service,注意看包名
import com.example.providerserver.pojo.bigdatatest;
import com.example.providerserver.service.BigdatatestService;
import org.apache.dubbo.config.annotation.Reference;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ConsumController {
@Reference
private BigdatatestService service;
@RequestMapping("/rpcTest")
public String rpcTest(){
for (bigdatatest bd:service.selectall()) {
System.out.println("===================");
System.out.println(bd.getId());
System.out.println(bd.getAge());
System.out.println(bd.getName());
System.out.println("===================");
}
return service.selectall().toString();
}
}
配置编写
server:
port: 8122
dubbo:
# 注册服务应用名字
application:
name: consumer-server
# 注册中心地址
registry:
address: zookeeper://127.0.0.1:2181
# 随机dubbo端口号,否则可能存在端口冲突的问题
protocol:
port: -1
启动项目
进入浏览器,输入localhost:8122
,可看到返回成功