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.

机翻后,就是(作者稍作修改):

  1. Thymeleaf是适用于Web和独立环境的现代服务器端Java模板引擎。

  2. Thymeleaf的主要目标是为您的开发工作流程带来优雅的模板,HTML可以在浏览器中正确显示,也可以作为静态原型工作,从而可以在开发团队中加强协作。

  3. Thymeleaf拥有用于Spring Framework的模块,与您喜欢的工具(例如springboot)的大量集成以及嵌入您自己的功能的能力,对于现代HTML5 JVM Web开发而言,Thymeleaf是理想的选择-尽管它还有很多工作要做。

不得不说Thymeleaf和JSP十分得像,但是他们之间的区别在于,不运行项目之前,Thymeleaf也是纯HTML(不需要服务端的支持)而JSP需要进行一定的转换,这样就方便前端人员进行独立的设计、调试。它有如下三个吸引人的特点:

  1. Thymeleaf 在有网络和无网络的环境下皆可运行,即它可以让美工在浏览器查看页面的静态效果,也可以让程序员在服务器查看带数据的动态页面效果。这是由于它支持 html 原型,然后在 html 标签里增加额外的属性来达到模板+数据的展示方式。浏览器解释 html 时会忽略未定义的标签属性,所以 thymeleaf 的模板可以静态地运行;当有数据返回到页面时,Thymeleaf 标签会动态地替换掉静态内容,使页面动态显示。

  2. Thymeleaf 开箱即用的特性。它提供标准和spring标准两种方言,可以直接套用模板实现JSTL、 OGNL表达式效果,避免每天套模板、该jstl、改标签的困扰。同时开发人员也可以扩展和创建自定义的方言。

  3. Thymeleaf 提供spring标准方言和一个与 SpringMVC 完美集成的可选模块,可以快速的实现表单绑定、属性编辑器、国际化等功能。

摘自:spring boot(四):thymeleaf使用详解-纯洁的微笑

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教程(10分钟入门) (biancheng.net)

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>

引用公共页面

可以使用以下指令

  1. th:replace:把独立的模板替换为主模板中的内容。

  2. th:insert:把独立的模板插入为主模板中的一部分。

  3. 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,详情如下

10分钟入门shiro

以下是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,按路径srcmainresources找到application.properties,编辑该文件。推荐用idea或者eclipse这些ide软件打开这些项目

添加server.port=7001,为这个项目修改端口,否则默认8080端口启动。注意admin.registry.addressadmin.root.user.name=rootadmin.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-serverapplication.propertiesadmin.root.user.nameadmin.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>

引入依赖--其他

  1. spring-boot-starter-web

  2. spring-boot-starter-jdbc

  3. Druid

  4. Mybatis

  5. 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)的搭建

引入依赖

  1. dubbo+zookeeper:与生产者的搭建一样

  2. 其他: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,可看到返回成功

文章作者: Vsoapmac
版权声明: 本站所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 soap的会员制餐厅
后端 框架
喜欢就支持一下吧