狂神。SpringBoot学习(2)

SpringBoot学习2

10、整合JDBC使用

10.1、Spring Data简介

对于数据访问层,无论是 SQL(关系型数据库) 还是 NOSQL(非关系型数据库),SpringBoot 底层都是采用 Spring Data 的方式进行统一处理。

SpringBoot 底层都是采用 Spring Data 的方式进行统一处理各种数据库,Spring Data 也是 Spring 中与 Spring Boot、Spring Cloud 等齐名的知名项目。

Sping Data 官网:https://spring.io/projects/spring-data

数据库相关的启动器 :可以参考官方文档:

https://docs.spring.io/spring-boot/docs/2.2.5.RELEASE/reference/htmlsingle/#using-boot-starter

10.2、默认的数据源

  1. 我们新建一个springboot项目: springboot-05-data ,并勾选Web、JDBC和数据库驱动模块。

在这里插入图片描述

  1. 项目建好之后,发现pom文件中自动帮我们导入了如下依赖:
<!--jdbc-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!--mysql-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>
  1. 新建一个 application.yaml 配置文件,编写数据库的配置:
spring:
  datasource:
    username: root
    password: newpass
    # 假如时区报错了,就增加一个时区的配置就ok了。   serverTimezone=UTC
    url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
    driver-class-name: com.mysql.cj.jdbc.Driver
    # type: com.alibaba.druid.pool.DruidDataSource # 自定义数据源
  1. 配置完之后,我们就可以直接去使用了,因为 SpringBoot 已经默认帮我们进行了自动配置。去测试类测试一下:
package com.kuang;

import com.alibaba.druid.pool.DruidDataSource;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;

@SpringBootTest
class Springboot04DataApplicationTests {

    @Autowired
    DataSource dataSource;

    @Test
    void contextLoads() throws SQLException {

        //查看以下默认的数据源:class com.zaxxer.hikari.HikariDataSource
        System.out.println(dataSource.getClass());

        //获得数据库连接。
        Connection connection = dataSource.getConnection();
        System.out.println(connection);

        //xxx Template:Springboot已经配置好模版bean,拿来即用 CURD

        //关闭
        connection.close();
    }

}

查看结果:我们可以看到默认给我们配置的数据源为 : class com.zaxxer.hikari.HikariDataSource

我们来全局搜索一下,找到数据源的所有自动配置都在 DataSourceAutoConfiguration 文件中:

@Import({ DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class,
    DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.OracleUcp.class,
    DataSourceConfiguration.Generic.class, DataSourceJmxConfiguration.class })
protected static class PooledDataSourceConfiguration {
}

这里导入的类都在 DataSourceConfiguration 配置类下,可以看出 SpringBoot 2.7.2 默认使用HikariDataSource 数据源;而以前版本,如 SpringBoot 1.5 默认使用 org.apache.tomcat.jdbc.pool.DataSource 作为数据源。

HikariDataSource 号称 Java WEB 当前速度最快的数据源,相比于传统的 C3P0 、DBCP、Tomcat jdbc 等连接池更加优秀。

可以使用 spring.datasource.type 指定自定义的数据源类型,值为要使用的连接池实现的完全限定名。

10.3、JDBCTemplate

  1. 有了数据源 (com.zaxxer.hikari.HikariDataSource),然后可以拿到数据库连接 (java.sql.Connection),有了连接,就可以使用原生的 JDBC 语句来操作数据库。

  2. 即使不使用第三方第数据库操作框架,如 MyBatis等,Spring本身也对原生的 JDBC 做了轻量级的封装,即JdbcTemplate。

  3. 数据库操作的所有 CRUD 方法都在 JdbcTemplate 中。

  4. Spring Boot 不仅提供了默认的数据源,同时默认已经配置好了 JdbcTemplate 放在了容器中,程序员只需自己注入即可使用。

  5. JdbcTemplate 的自动配置是依赖 org.springframework.boot.autoconfigure.jdbc 包下的 JdbcTemplateConfiguration 类。

JdbcTemplate 主要提供以下几类方法:

  • execute 方法:可以用于执行任何SQL语句,一般用于执行DDL语句。
  • update 方法及 batchUpdate 方法:update 方法用于执行新增、修改、删除等语句;batchUpdate方法用于执行批处理相关语句。
  • query 方法及 queryForXXX 方法:用于执行查询相关语句。
  • call 方法:用于执行存储过程、函数相关语句。

测试代码:

首先,我们需要连接数据库,用的是我们用的 Mybatis 数据库中的 user 表。

然后,我们编写一个类: JDBCController,并注入 jdbcTemplate:

package com.kuang.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;
import java.util.Map;

@RestController
public class JDBCController {

  @Autowired
  JdbcTemplate jdbcTemplate;

  //查询数据库的所有信息。
  //没有实体类,数据库中的东西,怎么获取?   Map
  @GetMapping("/userList")
  public List<Map<String,Object>> userList(){
      String sql = "select * from user";
      List<Map<String, Object>> list_maps = jdbcTemplate.queryForList(sql);
      return list_maps;
  }

  @GetMapping("/addUser")
  public String addUser(){
      String sql = "insert into mybatis.user(id,name,pwd) values (6,'小明','123456')";
      jdbcTemplate.update(sql);
      return "addUser-ok!";
  }

  @GetMapping("/updateUser/{id}")
  public String updateUser(@PathVariable("id") int id){
      String sql = "update mybatis.user set name=?,pwd=? where id="+id;
      //封装
      Object[] objects = new Object[2];
      objects[0]="小垃圾";
      objects[1]="777777";
      jdbcTemplate.update(sql,objects);
      return "updateUser-ok!";
  }

  @GetMapping("/deleteUser/{id}")
  public String deleteUser(@PathVariable("id") int id){
      String sql = "delete from mybatis.user where id=?";
      jdbcTemplate.update(sql,id);
      return "deleteUser-ok!";
  }

}

然后,我们启动项目,访问对应的请求进行测试,CURD 功能实现正常,JDBC搞定!

11、整合Druid数据源

11.1、Druid简介

Java程序很大一部分要操作数据库,为了提高性能操作数据库的时候,又不得不使用数据库连接池。

Druid 是阿里巴巴开源平台上一个数据库连接池实现,结合了 C3P0、DBCP 等 DB 池的优点,同时加入了日志监控。

Druid 可以很好的监控 DB 池连接和 SQL 的执行情况,天生就是针对监控而生的 DB 连接池。

Druid已经在阿里巴巴部署了超过600个应用,经过一年多生产环境大规模部署的严苛考验。

Spring Boot 2.0 以上默认使用 Hikari 数据源,可以说 Hikari 与 Driud 都是当前 Java Web 上最优秀的数据源,我们来重点介绍 Spring Boot 如何集成 Druid 数据源,如何实现数据库监控。

com.alibaba.druid.pool.DruidDataSource 基本配置参数如下:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

11.2、配置Druid数据源

  1. 添加上 Druid 数据源和 log4j 的依赖。
<!--druid-->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.21</version>
</dependency>
<!--log4j-->
<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>
  1. 切换数据源。之前说过 Spring Boot 2.0 以上默认使用 com.zaxxer.hikari.HikariDataSource 数据源,但可以通过 spring.datasource.type 指定数据源。
spring:
  datasource:
    username: root
    password: newpass
    # 假如时区报错了,就增加一个时区的配置就ok了。   serverTimezone=UTC
    url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
    driver-class-name: com.mysql.cj.jdbc.Driver
    
    # 自定义数据源
    type: com.alibaba.druid.pool.DruidDataSource 
  1. 数据源切换之后,在测试类再次进行测试,获取到他的类,查看输出是否成功切换:

在这里插入图片描述

  1. 切换成功后,就可以设置数据源连接初始化大小、最大连接数、等待时间、最小连接数等设置项。在 application.yaml 中添加以下代码:
    #Spring Boot 默认是不注入这些属性值的,需要自己绑定
    #druid 数据源专有配置
    initialSize: 5
    minIdle: 5
    maxActive: 20
    maxWait: 60000
    timeBetweenEvictionRunsMillis: 60000
    minEvictableIdleTimeMillis: 300000
    validationQuery: SELECT 1 FROM DUAL
    testWhileIdle: true
    testOnBorrow: false
    testOnReturn: false
    poolPreparedStatements: true

    #配置监控统计拦截的filters,stat:监控统计、log4j:日志记录、wall:防御sql注入
    #如果允许时报错  java.lang.ClassNotFoundException: org.apache.log4j.Priority
    #则导入 log4j 依赖即可,Maven 地址:https://mvnrepository.com/artifact/log4j/log4j
    filters: stat,wall,log4j
    maxPoolPreparedStatementPerConnectionSize: 20
    useGlobalDataSourceStat: true
    connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
  1. 然后,需要我们为 DruidDataSource 绑定全局配置文件中的参数,再添加到容器中,而不再使用 Spring Boot 的自动生成了。新建一个 DruidConfig,添加 DruidDataSource 组件到容器中,并绑定属性:
package com.kuang.config;

import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.support.http.StatViewServlet;
import com.alibaba.druid.support.http.WebStatFilter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.servlet.Filter;
import javax.sql.DataSource;
import java.util.HashMap;

@Configuration
public class DruidConfig {

    @ConfigurationProperties(prefix = "spring.datasource")
    @Bean
    public DataSource druidDataSource(){
        return new DruidDataSource();
    }
  
}

11.3、配置Druid数据源监控

Druid 数据源具有监控的功能,并提供了一个 web 界面方便用户查看。

首先,需要设置 Druid 的后台管理页面,比如登录账号、密码等信息。

在 DruidConfig 中添加:

//后台监控:web.xml,ServletRegistrationBean
//因为springboot内置了servlet容器,所以没有web.xml,替代方法:ServletRegistrationBean
@Bean
public ServletRegistrationBean a(){
    ServletRegistrationBean<StatViewServlet> bean = new ServletRegistrationBean<>(new StatViewServlet(), "/druid/*");
    //后台需要有人登陆,账号密码配置。
    HashMap<String, String> initParameters = new HashMap<>();

    //增加配置。
    //登陆key,是固定的。   loginUsername   loginPassword
    initParameters.put("loginUsername","admin");
    initParameters.put("loginPassword","123456");
    //允许谁可以访问。为空或者为null时,表示允许所有访问.
    initParameters.put("allow","");
    //禁止谁能访问。
    //initParameters.put("kuangshen","192.168.11.123");

    bean.setInitParameters(initParameters); //设置初始化参数。
    return bean;
}

配置完毕后,我们可以选择访问 :http://localhost:8080/druid 来到登陆页!

在这里插入图片描述

登陆成功后,我们再开一个网页输入一个更新的请求,我们会在SQL监控中查看到我们的操作信息!

在这里插入图片描述

配置 Druid web 监控 filter 过滤器:

在 DruidConfig 中添加:

//filter
@Bean
public FilterRegistrationBean webStatFilter(){
    FilterRegistrationBean bean = new FilterRegistrationBean();
    //WebStatFilter:用于配置Web和Druid数据源之间的管理关联监控统计
    bean.setFilter(new WebStatFilter());
    //可以过滤那些请求呢?
    HashMap<Object, Object> initParameters = new HashMap<>();
    //这些东西不能进行统计。
    initParameters.put("exclusions","*.js,*.css,/druid/*");
    bean.setInitParameters(initParameters);
    return bean;
}

12、整合Mybatis框架

首先,新建一个项目 springboot-06-mybatis,并勾选Web、JDBC和数据库驱动模块。

  1. 在pom文件中导入 MyBatis 和 SpringBoot 整合所需要的依赖:
<!--mybatis-spring-boot-starter:整合-->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.1</version>
</dependency>
  1. 在 application.properties 中编写数据库连接和整合 mybatis 的配置:
spring.datasource.username=root
spring.datasource.password=newpass
spring.datasource.url=jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

# 整合mybatis
mybatis.type-aliases-package=com.kuang.pojo
mybatis.mapper-locations=classpath:mybatis/mapper/*.xml
  1. 编写一个 User 实体类:(需要引入lombok依赖)
package com.kuang.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {

    private int id;
    private String name;
    private String pwd;

}
  1. 创建 Mapper 接口:UserMapper
package com.kuang.mapper;

import com.kuang.pojo.User;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;

import java.util.List;

//这个注解表示了这是一个 mybatis 的 mapper 类:Dao
@Mapper
@Repository
public interface UserMapper {

    List<User> queryUserList();

    User queryUserById(int id);

    int addUser(User user);

    int updateUser(User user);

    int deleteUser(int id);

}
  1. 编写对应的 Mapper 映射文件:UserMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.kuang.mapper.UserMapper">

    <select id="queryUserList" resultType="User">
        select * from user
    </select>

    <select id="queryUserById" resultType="User">
        select * from user where id = #{id};
    </select>

    <insert id="addUser" parameterType="User">
        insert into user (id,name,pwd) values (#{id},#{name},#{pwd});
    </insert>

    <update id="updateUser" parameterType="User">
        update user set name=#{name},pwd=#{pwd} where id=#{id}
    </update>

    <delete id="deleteUser" parameterType="int">
        delete from user where id=#{id}
    </delete>

</mapper>
  1. 编写对应的 controller 类:Usercontroller
package com.kuang.controller;

import com.kuang.mapper.UserMapper;
import com.kuang.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
public class Usercontroller {

    @Autowired
    UserMapper userMapper;

    //查询全部用户。
    @GetMapping("/queryUserList")
    public List<User> queryUserList(){
        List<User> userList = userMapper.queryUserList();
        for (User user : userList) {
            System.out.println(user);
        }
        return userList;
    }

    //通过id查询用户
    @GetMapping("/queryUserById/{id}")
    public User queryUserById(@PathVariable("id")int id){
        User user = userMapper.queryUserById(id);
        return user;
    }

    //添加一个用户
    @GetMapping("/addUser")
    public String addUser(){
        userMapper.addUser(new User(6,"小垃圾","888888"));
        return "addUser-OK!";
    }

    //修改一个用户
    @GetMapping("/updateUser")
    public String updateUser(){
        userMapper.updateUser(new User(6,"labuladuo","666666"));
        return "updateUser-OK!";
    }

    //删除一个用户
    @GetMapping("/deleteUser")
    public String deleteUser(){
        userMapper.deleteUser(6);
        return "deleteUser-OK!";
    }

}
  1. 连接数据库,启动项目,进行测试,查看结果,整合成功!

13、集成SpringSecurity

13.1、安全简介

在 Web 开发中,安全一直是非常重要的一个方面。安全虽然属于应用的非功能性需求,但是应该在应用开发的初期就考虑进来。如果在应用开发的后期才考虑安全的问题,就可能陷入一个两难的境地:一方面,应用存在严重的安全漏洞,无法满足用户的要求,并可能造成用户的隐私数据被攻击者窃取;另一方面,应用的基本架构已经确定,要修复安全漏洞,可能需要对系统的架构做出比较重大的调整,因而需要更多的开发时间,影响应用的发布进程。因此,从应用开发的第一天就应该把安全相关的因素考虑进来,并在整个应用的开发过程中。

市面上存在比较有名的:Shiro,Spring Security

这里需要阐述一下的是,每一个框架的出现都是为了解决某一问题而产生了,那么Spring Security框架的出现是为了解决什么问题呢?

首先我们看下它的官网介绍:

  • Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架。它实际上是保护基于spring的应用程序的标准。

  • Spring Security是一个框架,侧重于为Java应用程序提供身份验证和授权。与所有Spring项目一样,Spring安全性的真正强大之处在于它可以轻松地扩展以满足定制需求。

从官网的介绍中可以知道这是一个权限框架。想我们之前做项目是没有使用框架是怎么控制权限的?对于权限一般会细分为功能权限,访问权限,和菜单权限。代码会写的非常的繁琐,冗余。

怎么解决之前写权限代码繁琐,冗余的问题,一些主流框架就应运而生而Spring Scecurity就是其中的一种。

Spring 是一个非常流行和成功的 Java 应用开发框架。Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性的完整解决方案。一般来说,Web 应用的安全性包括用户认证(Authentication)和用户授权(Authorization)两个部分。用户认证指的是验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。用户授权指的是验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。

对于上面提到的两种应用情景,Spring Security 框架都有很好的支持。在用户认证方面,Spring Security 框架支持主流的认证方式,包括 HTTP 基本认证、HTTP 表单验证、HTTP 摘要认证、OpenID 和 LDAP 等。在用户授权方面,Spring Security 提供了基于角色的访问控制和访问控制列表(Access Control List,ACL),可以对应用中的领域对象进行细粒度的控制。

13.2、SpringSecurity简介

参考官网:https://spring.io/projects/spring-security

Spring Security 是针对Spring项目的安全框架,也是Spring Boot底层安全模块默认的技术选型,他可以实现强大的Web安全控制,对于安全控制,我们仅需要引入 spring-boot-starter-security 模块,进行少量的配置,即可实现强大的安全管理!

记住几个类:

  • WebSecurityConfigurerAdapter:自定义Security策略
  • AuthenticationManagerBuilder:自定义认证策略
  • @EnableWebSecurity:开启WebSecurity模式

Spring Security的两个主要目标是 “认证” 和 “授权”(访问控制)。

“认证”(Authentication)

身份验证是关于验证您的凭据,如用户名/用户ID和密码,以验证您的身份。

身份验证通常通过用户名和密码完成,有时与身份验证因素结合使用。

“授权” (Authorization)

授权发生在系统成功验证您的身份后,最终会授予您访问资源(如信息,文件,数据库,资金,位置,几乎任何内容)的完全权限。

这个概念是通用的,而不是只在 Spring Security 中存在。

13.3、测试环境搭配

  1. 新建一个初始的springboot项目:springboot-07-security,并勾选 web 模块和 thymeleaf 模块。

  2. 导入静态资源,导入后的目录结果如下所示:

在这里插入图片描述

  1. 新建一个 RouterController 来编写对应的 controller:
package com.kuang.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class RouterController {

    @RequestMapping({"/","/index"})
    public String index(){
        return "index";
    }

    @RequestMapping("/toLogin")
    public String toLogin(){
        return "views/login";
    }

    @RequestMapping("/level1/{id}")
    public String level1(@PathVariable("id") int id) {
        return "views/level1/"+id;
    }

    @RequestMapping("/level2/{id}")
    public String level2(@PathVariable("id") int id) {
        return "views/level2/"+id;
    }

    @RequestMapping("/level3/{id}")
    public String level3(@PathVariable("id") int id) {
        return "views/level3/"+id;
    }

}	
  1. 在 application.properties 中配置关闭 thymeleaf 的缓存:
spring.thymeleaf.cache=false
  1. 启动项目,进行测试,环境搭建成功!

在这里插入图片描述

13.4、认证和授权

目前,我们的测试环境,是谁都可以访问的,我们使用 Spring Security 增加上认证和授权的功能。

  1. 引入 Spring Security 模块:
<!--security-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
  1. 编写 Spring Security 的基础配置类:(参考官网)
package com.kuang.config;

import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
    }

}
  1. 定制请求的授权规则:
//链式编程
@Override
protected void configure(HttpSecurity http) throws Exception {
    //首页所有人可以访问,功能页只有对应有权限的人才能访问!
    //请求授权的规则~
    http.authorizeRequests()
            .antMatchers("/").permitAll()
            .antMatchers("/level1/**").hasRole("vip1")
            .antMatchers("/level2/**").hasRole("vip2")
            .antMatchers("/level3/**").hasRole("vip3")
}
  1. 启动项目,测试一下:发现除了首页都进不去了!因为我们目前没有登录的角色,请求需要登录的角色拥有对应的权限才可以访问!

  2. 在上面的方法中加入以下配置,开启自动配置的登录功能!

//没有权限默认会到登陆页面,需要开启登陆的页面。   login.html
http.formLogin();
  1. 重启项目,测试一下:发现没有权限的时候,会跳转到登录的页面!

  2. 我们可以定义认证规则,重写 configure(AuthenticationManagerBuilder auth) 方法:

//认证
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
   //这些数据正常应该从数据库中读。
   auth.inMemoryAuthentication()
          .withUser("kuangshen").password("123456").roles("vip2","vip3")
          .and()
          .withUser("root").password("123456").roles("vip1","vip2","vip3")
          .and()
          .withUser("guest").password("123456").roles("vip1","vip2");
}
  1. 重启项目,测试一下:我们可以使用这些账号登录进行测试,发现会报错:

There is no PasswordEncoder mapped for the id “null”

在这里插入图片描述

  1. 原因,我们要将前端传过来的密码进行某种方式加密,否则就无法登录。我们修改上面的代码:
//认证
//密码编码:PasswordEncoder
//在Spring Security 5.0+ 新增了很多的方法~
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    //这些数据正常应该从数据库中读。
    auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
            .withUser("kuangshen").password(new BCryptPasswordEncoder().encode("123456")).roles("vip2","vip3")
            .and()
            .withUser("root").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2","vip3")
            .and()
            .withUser("guest").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1");
}
  1. 重启项目,测试一下:登录成功,并且每个角色只能访问自己认证下的规则!

==注意:==我们在测试的使用有两个登陆页面,一个是 SpringSecurity 自带的,一个是我们自己定制的。我们现在用的是SpringSecurity 自带的,我们自己定制的后面介绍它的设置使用。

13.5、权限控制和注销

  1. 在 configure(HttpSecurity http) 方法中开启自动配置的注销的功能:
//注销,开启了注销功能。
http.logout();
  1. 我们在前端,在 index.html 导航栏中添加增加一个注销的按钮:
<a class="item" th:href="@{/logout}">
    <i class="sign-out icon"></i> 注销
</a>
  1. 我们启动项目测试一下,登录成功后点击注销,发现注销完毕会跳转到登录页面!

  2. 但是,我们想让他注销成功后,依旧可以跳转到首页。修改上面的代码:

//注销,开启了注销功能,跳转首页。
http.logout().logoutSuccessUrl("/");
  1. 重启项目进行测试,注销完毕后,发现跳转到首页!

  2. 我们现在又来一个需求:用户没有登录的时候,导航栏上只显示登录按钮,用户登录之后,导航栏可以显示登录的用户信息及注销按钮!还有就是,比如 kuangshen 这个用户,它只有 vip2,vip3功能,那么登录则只显示这两个功能,而vip1的功能菜单不显示!这个就是真实的网站情况了!该如何做呢?

我们需要结合 thymeleaf 中的一些功能:

sec:authorize="isAuthenticated()" : 是否认证登录,来显示不同的页面。

我们需要添加 thymeleaf 和 springsecurity 整合的依赖:

<!--thymeleaf-springsecurity 整合-->
<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity4</artifactId>
    <version>3.0.4.RELEASE</version>
</dependency>
  1. 修改我们的前端页面:

导入命名空间:

xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4"

修改导航栏,增加认证判断:

<!--登录注销-->
<div class="right menu">
  
    <!--如果未登陆-->
    <div sec:authorize="! isAuthenticated()">
        <a class="item" th:href="@{/login}">
            <i class="address card icon"></i> 登录
        </a>
    </div>
  
    <!--如果登陆:用户名,注销-->
    <div sec:authorize="isAuthenticated()">
        <a class="item">
            用户名:<span sec:authentication="name"></span>
        </a>
    </div>
  
    <div sec:authorize="isAuthenticated()">
        <a class="item" th:href="@{/logout}">
            <i class="sign-out icon"></i> 注销
        </a>
    </div>

</div>

使菜单根据用户的角色动态的实现:

<!--菜单根据用户的角色动态的实现~-->
<div class="column" sec:authorize="hasRole('vip1')">
    <div class="ui raised segment">
        <div class="ui">
            <div class="content">
                <h5 class="content">Level 1</h5>
                <hr>
                <div><a th:href="@{/level1/1}"><i class="bullhorn icon"></i> Level-1-1</a></div>
                <div><a th:href="@{/level1/2}"><i class="bullhorn icon"></i> Level-1-2</a></div>
                <div><a th:href="@{/level1/3}"><i class="bullhorn icon"></i> Level-1-3</a></div>
            </div>
        </div>
    </div>
</div>

<div class="column" sec:authorize="hasRole('vip2')">
    <div class="ui raised segment">
        <div class="ui">
            <div class="content">
                <h5 class="content">Level 2</h5>
                <hr>
                <div><a th:href="@{/level2/1}"><i class="bullhorn icon"></i> Level-2-1</a></div>
                <div><a th:href="@{/level2/2}"><i class="bullhorn icon"></i> Level-2-2</a></div>
                <div><a th:href="@{/level2/3}"><i class="bullhorn icon"></i> Level-2-3</a></div>
            </div>
        </div>
    </div>
</div>

<div class="column" sec:authorize="hasRole('vip3')">
    <div class="ui raised segment">
        <div class="ui">
            <div class="content">
                <h5 class="content">Level 3</h5>
                <hr>
                <div><a th:href="@{/level3/1}"><i class="bullhorn icon"></i> Level-3-1</a></div>
                <div><a th:href="@{/level3/2}"><i class="bullhorn icon"></i> Level-3-2</a></div>
                <div><a th:href="@{/level3/3}"><i class="bullhorn icon"></i> Level-3-3</a></div>
            </div>
        </div>
    </div>
</div>
  1. 重启项目进行测试,我们登录成功后,成功显示了我们想要的页面!

  2. 如果注销404了,就是因为它默认防止csrf跨站请求伪造,因为会产生安全问题,我们可以将请求改为post表单提交,或者在 spring security 中关闭csrf功能。我们在 configure(HttpSecurity http) 方法中添加:

//防止网站工具:get,post
http.csrf().disable(); //关闭csrf功能。,登陆失败肯存在的原因。
  1. 重启项目进行测试,权限控制和注销成功!

==注意:==如果我们出现问题的话就降低 springboot 的版本,在 pom.xml 文件中直接修改后刷新:

在这里插入图片描述

13.6、记住我功能实现

我们现在只要登录之后,关闭浏览器,再登录,就会让我们重新登录。但是很多网站会有一个记住密码的功能。

  1. 开启记住我功能
//开启记住我功能。cookie.默认保存两周,自定义介接受前端的参数。
http.rememberMe().rememberMeParameter("remember");
  1. 我们重启项目测试一下,发现登录页多了一个记住我功能,我们勾选他登录之后关闭浏览器,然后重新打开浏览器访问,发现用户依旧存在!

思考:如何实现的呢?其实非常简单。

我们可以查看浏览器的 cookie,发现他给了我们一个 cookie。 我们点击注销的时候,spring security 帮我们自动删除了这个 cookie。

在这里插入图片描述

结论:登录成功后,将cookie发送给浏览器保存,以后登录带上这个cookie,只要通过检查就可以免登录了。如果点击注销,则会删除这个cookie,具体的原理我们在 JavaWeb 阶段都讲过了,这里就不在多说了!

13.7、定制登录页

现在这个登录页面都是 spring security 默认的,怎么样可以使用我们自己写的 login 界面呢?

  1. 在刚才的登录页配置后面指定 loginpage:
http.formLogin().loginPage("/toLogin");
  1. 然后前端 inedx.html 中也需要指向我们自己定义的 login请求:
<a class="item" th:href="@{/toLogin}">
    <i class="address card icon"></i> 登录
</a>
  1. 我们登录,需要将这些信息发送到哪里,我们也需要配置,login.html 配置提交请求及方式,方式必须为post。在 loginPage() 源码中的注释上有写明:

在这里插入图片描述

修改 login.html 中表单的代码:

<form th:action="@{/login}" method="post">
    <div class="field">
        <label>Username</label>
        <div class="ui left icon input">
            <input type="text" placeholder="Username" name="user">
            <i class="user icon"></i>
        </div>
    </div>
    <div class="field">
        <label>Password</label>
        <div class="ui left icon input">
            <input type="password" name="pwd">
            <i class="lock icon"></i>
        </div>
    </div>

    <!--记住我-->
    <div class="field">
        <input type="checkbox" name="remember">记住我
    </div>

    <input type="submit" class="ui blue submit button"/>
</form>
  1. 这个请求提交上来,我们还需要验证处理。我们可以查看 formLogin() 方法的源码。我们配置接收登录的用户名和密码的参数!
//没有权限默认会到登陆页面,需要开启登陆的页面。   login.html
//定制登陆页  loginPage("/toLogin")
//走的"/toLogin",但真正走的"/login"进行验证处理。
//接受的参数是 user 和 pwd 。
http.formLogin().loginPage("/toLogin")
        .usernameParameter("user").passwordParameter("pwd")
        .loginProcessingUrl("/login");
  1. 在登录页增加记住我的多选框。(上面代码中已经加上了)

  2. 重启项目进行测试,测试成功!


完整代码

package com.kuang.config;

import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;


//AOP:拦截器!
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    //链式编程
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //首页所有人可以访问,功能页只有对应有权限的人才能访问!
        //请求授权的规则~
        http.authorizeRequests()
                .antMatchers("/").permitAll()
                .antMatchers("/level1/**").hasRole("vip1")
                .antMatchers("/level2/**").hasRole("vip2")
                .antMatchers("/level3/**").hasRole("vip3");

        //没有权限默认会到登陆页面,需要开启登陆的页面。   login.html
        //定制登陆页  loginPage("/toLogin")
        //走的"/toLogin",但真正走的是"/login"。
        //接受的参数是 user 和 pwd 。
        http.formLogin().loginPage("/toLogin")
                .usernameParameter("user").passwordParameter("pwd")
                .loginProcessingUrl("/login");

        //注销,开启了注销功能,跳转首页。
        http.logout().logoutSuccessUrl("/");

        //防止网站工具:get,post
        http.csrf().disable(); //关闭csrf功能。,登陆失败肯存在的原因。

        //开启记住我功能。cookie.默认保存两周,自定义介接受前端的参数。
        http.rememberMe().rememberMeParameter("remember");

    }

    //认证
    //密码编码:PasswordEncoder
    //在Spring Security 5.0+ 新增了很多的方法~
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {

        //这些数据正常应该从数据库中读。
        auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
                .withUser("kuangshen").password(new BCryptPasswordEncoder().encode("123456")).roles("vip2","vip3")
                .and()
                .withUser("root").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2","vip3")
                .and()
                .withUser("guest").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1");
    }

}

14、Shiro

14.1、概述

简介

Apache Shiro 是一个强大且易用的 Java 安全框架。

可以完成身份验证、授权、密码和会话管理。

Shiro 不仅可以用在 JavaSE 环境中,也可以用在 JavaEE 环境中。

官网: http://shiro.apache.org/

功能

在这里插入图片描述

Authentication:身份认证/登录,验证用户是不是拥有相应的身份;

Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;

Session Manager:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通 JavaSE 环境的,也可以是如 Web 环境的;

Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;

Web Support:Web 支持,可以非常容易的集成到 Web 环境;

Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率;

Concurrency:shiro支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;

Testing:提供测试支持;

Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;

Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。

从外部看

在这里插入图片描述

应用代码直接交互的对象是Subject,也就是说Shiro的对外API核心就是Subject。

其每个API的含义:

Subject:主体,代表了当前“用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是Subject,如网络爬虫,机器人等;即一个抽象概念;所有Subject都绑定到SecurityManager,与Subject的所有交互都会委托给SecurityManager;可以把Subject认为是一个门面;SecurityManager才是实际的执行者。

SecurityManager:安全管理器;即所有与安全有关的操作都会与SecurityManager交互;且它管理着所有Subject;可以看出它是Shiro的核心,它负责与后边介绍的其他组件进行交互,如果学习过SpringMVC,你可以把它看成DispatcherServlet前端控制器。

Realm:域,Shiro从从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm看成DataSource,即安全数据源。

也就是说对于我们而言,最简单的一个Shiro应用:

  1. 应用代码通过Subject来进行认证和授权,而Subject又委托给SecurityManager;
  2. 我们需要给Shiro的SecurityManager注入Realm,从而让SecurityManager能得到合法的用户及其权限进行判断。

从以上也可以看出,Shiro不提供维护用户/权限,而是通过Realm让开发人员自己注入。

外部架构

在这里插入图片描述

Subject:主体,可以看到主体可以是任何可以与应用交互的“用户”;

SecurityManager:相当于SpringMVC中的DispatcherServlet或者Struts2中的FilterDispatcher;是Shiro的心脏;所有具体的交互都通过SecurityManager进行控制;它管理着所有Subject、且负责进行认证和授权、及会话、缓存的管理。

Authenticator:认证器,负责主体认证的,这是一个扩展点,如果用户觉得Shiro默认的不好,可以自定义实现;其需要认证策略(Authentication Strategy),即什么情况下算用户认证通过了;

Authrizer:授权器,或者访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中的哪些功能;

Realm:可以有1个或多个Realm,可以认为是安全实体数据源,即用于获取安全实体的;可以是JDBC实现,也可以是LDAP实现,或者内存实现等等;由用户提供;注意:Shiro不知道你的用户/权限存储在哪及以何种格式存储;所以我们一般在应用中都需要实现自己的Realm;

SessionManager:如果写过Servlet就应该知道Session的概念,Session呢需要有人去管理它的生命周期,这个组件就是SessionManager;而Shiro并不仅仅可以用在Web环境,也可以用在如普通的JavaSE环境、EJB等环境;所有呢,Shiro就抽象了一个自己的Session来管理主体与应用之间交互的数据;这样的话,比如我们在Web环境用,刚开始是一台Web服务器;接着又上了台EJB服务器;这时想把两台服务器的会话数据放到一个地方,这个时候就可以实现自己的分布式会话(如把数据放到Memcached服务器);

SessionDAO:DAO大家都用过,数据访问对象,用于会话的CRUD,比如我们想把Session保存到数据库,那么可以实现自己的SessionDAO,通过如JDBC写到数据库;比如想把Session放到Memcached中,可以实现自己的Memcached SessionDAO;另外SessionDAO中可以使用Cache进行缓存,以提高性能;

CacheManager:缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据基本上很少去改变,放到缓存中后可以提高访问的性能;

Cryptography:密码模块,Shiro提高了一些常见的加密组件用于如密码加密/解密的。

认证流程

在这里插入图片描述

用户 提交 身份信息、凭证信息 封装成 令牌 交由 安全管理器 认证。

14.2、Shiro快速开始

官网的十分钟入门:https://shiro.apache.org/10-minute-tutorial.html

我们可以按照官网的提示下载对应版本的 shiro (我的版本是1.4.1)。

quickstart 对应的文件在 samples/quickstart 目录下,我们接下来的操作需要参考这个文件夹。

  1. 新建一个 Maven 项目:springboot-08-shiro,删除 src 目录,将其作为父工程。
  2. 在父目录下新建一个模块:hello-shiro,导入相关的依赖:
<dependencies>
    <!--shiro-->
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-core</artifactId>
        <version>1.4.1</version>
    </dependency>

    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>jcl-over-slf4j</artifactId>
        <version>1.7.21</version>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-log4j12</artifactId>
        <version>1.7.21</version>
    </dependency>

    <!--log4j-->
    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>1.2.17</version>
    </dependency>
</dependencies>
  1. 把 resources 下的 log4j.properties、shiro.ini 文件拷贝到我们项目来,这里会提示让我们下载一个 Ini插件,我们直接点击下载即可。
  2. 把 Quickstart.java 导入过来,运行一下main方法,运行成功!

最后我们的项目目录如下:

在这里插入图片描述

**注意:**如果项目运行出现问题,试一下以下操作:

//把Quickstart里的这部分代码:
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);

//替换为以下代码:
DefaultSecurityManager securityManager = new DefaultSecurityManager();
IniRealm iniRealm = new IniRealm("classpath:shiro.ini");
securityManager.setRealm(iniRealm);

接下来我们分析一下 Quickstart 这个类:

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.realm.text.IniRealm;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Factory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Quickstart {

    private static final transient Logger log = LoggerFactory.getLogger(Quickstart.class);

    public static void main(String[] args) {

        //固定写法
        Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
        SecurityManager securityManager = factory.getInstance();
        SecurityUtils.setSecurityManager(securityManager);
        /*DefaultSecurityManager securityManager = new DefaultSecurityManager();
        IniRealm iniRealm = new IniRealm("classpath:shiro.ini");
        securityManager.setRealm(iniRealm);*/


        // Now that a simple Shiro environment is set up, let's see what you can do:
        //核心代码:

        //1。获取当前的用户对象 Subject
        Subject currentUser = SecurityUtils.getSubject();

        //2。通过当前的用户拿到 Session
        Session session = currentUser.getSession();
        session.setAttribute("someKey", "aValue");
        String value = (String) session.getAttribute("someKey");
        if (value.equals("aValue")) {
            log.info("session => session [" + value + "]");
        }

        //3。判断当前用户是否被认证
        if (!currentUser.isAuthenticated()) {

            // Token:令牌
            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):
        //6。打印其标识主体
        log.info("User [" + currentUser.getPrincipal() + "] logged in successfully.");

        //test a role:
        //7。判断是否拥有角色
        if (currentUser.hasRole("schwartz")) {
            log.info("May the Schwartz be with you!");
        } else {
            log.info("Hello, mere mortal.");
        }

        //7。判断是否有权限
        //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!");
        }

        //8。注销
        currentUser.logout();

        //结束
        System.exit(0);
    }
}

14.3、Springboot集成Shiro环境搭配

  1. 在刚才的父项目中新建一个 springboot 模块:springboot-shiro。
  2. 导入 SpringBoot 和 Shiro 整合包、thymeleaf 的依赖:
<!--shiro整合spring的包-->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.3.2</version>
</dependency>

<!--thymeleaf-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
  1. 新建 config 文件夹,在这个文件夹下新建 UserRealm 类并继承 AuthorizingRealm ,然后重写两个方法:分别为授权和认证。
//自定义的 UserRealm ,需要继承AuthorizingRealm类。
public class UserRealm extends AuthorizingRealm {

    //授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("执行了=》授权doGetAuthorizationInfo");
        return null;
    }

    //认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        System.out.println("执行了=》认证doGetAuthenticationInfo");
		    return null;
    }
  
}
  1. 在 config 文件夹下新建 ShiroConfig 配置类,将三大 Bean 注入到 spring 容器中。

Shiro 三大要素:

  • Subjuet 用户
  • SecurityManager 管理所有用户
  • Realm 连接数据

首先,我们先创建 Realm 对象:

//创建 realm 对象,需要自定义类。第一步
@Bean
public UserRealm userRealm(){
    return new UserRealm();
}

然后,我们创建 SecurityManager,这里需要用到前面创建的 Realm,我们使用传参的方式关联 UserRealm:

//DefaultWebSecurityManager 。第二步
@Bean(name = "securityManager")
public DefaultWebSecurityManager defaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm){
    DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
    //关联UserRealm
    securityManager.setRealm(userRealm);
    return securityManager;
}

最后,我们创建 Subject,需要传入一个刚刚创建的 securityManager 。

//ShiroFilterFactoryBean 。第三步
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager){
    ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
    //设置安全管理器
    bean.setSecurityManager(defaultWebSecurityManager);
    return bean;
}
  1. 我们新建一个 MyController 来控制视图的跳转:
@Controller
public class MyController {

    @RequestMapping({"/","/index"})
    public String toIndex(Model model){
        model.addAttribute("msg","hello,shiro!");
        return "index";
    }

    @RequestMapping("/user/add")
    public String add(){
        return "user/add";
    }

    @RequestMapping("/user/update")
    public String update(){
        return "user/update";
    }

}
  1. 我们新建 index、add、update 页面,add 和 update 页面放在一个新建的 user 目录下:

index.html:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml"
                xmlns:shiro="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>


<h1>首页</h1>
<p th:text="${msg}"></p>

<hr>

<a th:href="@{/user/add}">add</a> | <a th:href="@{/user/update}">update</a>

</body>
</html>

add.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

<h1>add</h1>

</body>
</html>

update.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

<h1>update</h1>

</body>
</html>
  1. 启动项目,测试一下能否正常运行,测试成功!

14.4、shiro实现登录拦截

  1. 修改我们的 ShiroFilterFactoryBean,添加登陆拦截:
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager){
    ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
    //设置安全管理器
    bean.setSecurityManager(defaultWebSecurityManager);

    //添加shiro的内置过滤器
    /*
    anon:无需认证就可以访问。
    authc:必须认证了才能访问。
    user:必须拥有 记住我 功能才能访问。
    perms:拥有对某个资源的权限才能访问。
    role:拥有某个角色权限才能访问。
     */
    //拦截
    Map<String, String> filterMap = new LinkedHashMap<>();
    filterMap.put("/user/*","authc");
    bean.setFilterChainDefinitionMap(filterMap);
    //设置登陆的请求。
    bean.setLoginUrl("/toLogin");

    return bean;
}
  1. 新建一个登陆页面 login.html:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

<h1>登陆</h1>
<hr>

<form action="">
    <p>用户名:<input type="text" name="username"></p>
    <p>密码:<input type="text" name="password"></p>
    <p><input type="submit"></p>
</form>

</body>
</html>
  1. 在 MyController 中编写对应的 controller:
@RequestMapping("/toLogin")
public String toLogin(){
    return "login";
}
  1. 启动项目,进行测试,当我们访问 add 和 update 页面时,会被返回到 login 页面,测试成功!

14.5、实现用户认证

  1. 我们在 MyController 中编写一个登陆方法:
@RequestMapping("/login")
public String login(String username,String password,Model model){
    //获取当前用户。
    Subject subject = SecurityUtils.getSubject();
    //封装用户的登录数据。
    UsernamePasswordToken token = new UsernamePasswordToken(username,password);
    try {
        //执行登陆方法,如果没有异常就说明OK了!
        subject.login(token);
        return "index";
    } catch (UnknownAccountException e) { //用户名不存在。
        model.addAttribute("msg","用户名错误!");
        return "login";
    }catch (IncorrectCredentialsException e) { //密码不存在。
        model.addAttribute("msg","密码错误!");
        return "login";
    }
}
  1. 修改 login 页面,加上表单提交的 /login 请求,并且显示传过来的 msg 信息。
<p th:text="${msg}" style="color: red"></p>

<form th:action="@{/login}">
  1. 修改 UserRealm 中的认证方法:
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    System.out.println("执行了=》认证doGetAuthenticationInfo");

    //===========伪造数据库====================
    //用户名,密码。 数据中取
    String name = "root";
    String password = "123456";
    UsernamePasswordToken userToken = (UsernamePasswordToken) token;
    if (!userToken.getUsername().equals(name)){
        return null;//抛出异常:UnknownAccountException
    }
    //密码认证,shiro来做~
    return new SimpleAuthenticationInfo("",password,"");
  
}
  1. 重启项目进行测试,输入错误的用户名和密码时,会提示错误信息;输入错误的用户名和密码时,登陆成功!

14.6、Shiro整合Mybatis

连接我们的数据库,用的 mybatis 中的 user 表。

  1. 首先,导入需要的依赖:
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.21</version>
</dependency>

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.0</version>
</dependency>

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.24</version>
</dependency>
  1. 新建一个 application.yaml ,配置连接数据库和数据源的信息。
spring:
  datasource:
    username: root
    password: newpass
    # 假如时区报错了,就增加一个时区的配置就ok了。   serverTimezone=UTC
    url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource # 自定义数据源


    #Spring Boot 默认是不注入这些属性值的,需要自己绑定
    #druid 数据源专有配置
    initialSize: 5
    minIdle: 5
    maxActive: 20
    maxWait: 60000
    timeBetweenEvictionRunsMillis: 60000
    minEvictableIdleTimeMillis: 300000
    validationQuery: SELECT 1 FROM DUAL
    testWhileIdle: true
    testOnBorrow: false
    testOnReturn: false
    poolPreparedStatements: true

    #配置监控统计拦截的filters,stat:监控统计、log4j:日志记录、wall:防御sql注入
    #如果允许时报错  java.lang.ClassNotFoundException: org.apache.log4j.Priority
    #则导入 log4j 依赖即可,Maven 地址:https://mvnrepository.com/artifact/log4j/log4j
    filters: stat,wall,log4j
    maxPoolPreparedStatementPerConnectionSize: 20
    useGlobalDataSourceStat: true
    connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
  1. 在 application.properties 配置 mybatis :
mybatis.type-aliases-package=com.kuang.pojo
mybatis.mapper-locations=classpath:mapper/*.xml
  1. 新建一个 User 实体类:
package com.kuang.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {

    private int id;
    private String name;
    private String pwd;
    private String perms;

}
  1. 新建一个 UserMapper:
package com.kuang.mapper;

import com.kuang.pojo.User;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;

@Repository
@Mapper
public interface UserMapper {

    public User queryUserByName(String name);

}
  1. 在 resources 下新建一个 mapper 文件夹,在该文件夹下编写对应的 UserMapper.xml:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.kuang.mapper.UserMapper">

    <select id="queryUserByName" parameterType="String" resultType="User">
        select * from mybatis.user where name = #{name}
    </select>

</mapper>
  1. 新建一个 UserService :
package com.kuang.service;

import com.kuang.pojo.User;

public interface UserService {

    public User queryUserByName(String name);

}
  1. 新建对应的 UserServiceImpl:
package com.kuang.service;
import com.kuang.mapper.UserMapper;
import com.kuang.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl implements UserService{

    @Autowired
    UserMapper userMapper;

    @Override
    public User queryUserByName(String name) {
        return userMapper.queryUserByName(name);
    }

}
  1. 我们在测试类里测试一下:
package com.kuang;

import com.kuang.service.UserServiceImpl;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class SpringbootShiroApplicationTests {

    @Autowired
    UserServiceImpl userService;

    @Test
    void contextLoads() {
        System.out.println(userService.queryUserByName("狂神"));
    }

}
  1. 查看结果,测试成功!

在这里插入图片描述

  1. 接下来修改 UserRealm,注入 Service 层,把之前我们伪造的数据库改成真实的数据库:
@Autowired
UserService userService;

//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    System.out.println("执行了=》认证doGetAuthenticationInfo");  

    //===========连接真实的数据库====================
    UsernamePasswordToken userToken = (UsernamePasswordToken) token;
    User user = userService.queryUserByName(userToken.getUsername());
    if (user==null){  //没有这个人
        return null;  //UnknownAccountException
    }
    //密码可以使用 MD5 进行加密。
    //密码认证,shiro来做~
    return new SimpleAuthenticationInfo("",user.getPwd(),"");

}
  1. 重启项目,测试一下,我们使用数据库中的用户名和密码登录,登陆成功!

14.7、shiro请求授权实现

  1. 在 ShiroConfig 的 ShiroFilterFactoryBean 中添加代码,使授权才能访问对应的资源:
//授权,正常情况下,没有授权会跳转到未授权页面。
filterMap.put("/user/add","perms[user:add]");
filterMap.put("/user/update","perms[user:update]");
  1. 我们启动项目,再次访问对应的请求,发现会报一个401(未授权)的错误:

在这里插入图片描述

  1. 接下来我们要在 UserRealm 中编写授权功能,修改授权方法的代码:
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    System.out.println("执行了=》授权doGetAuthorizationInfo");

    SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
    info.addStringPermission("user:add");
    info.addStringPermission("user:update");
    return info;
}
  1. 再次进行测试,发现可以正常访问了。但是这样的话所有的用户都有这个权限,但是我们不想让所有用户都有某个权限要怎么实现呢?首先我们先去数据表中添加一个权限字段,然后取手动添加权限:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

  1. 接下来我们要在数据库中得到用户的授权。首先,在认证方法中,我们把获取到的 user 传入到资源中去:
return new SimpleAuthenticationInfo(user,user.getPwd(),"");
  1. 这样,user 这个资源就传递到了当前用户 Subject 整体资源中。修改授权方法中的代码:
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    System.out.println("执行了=》授权doGetAuthorizationInfo");

    SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
    //拿到当前登陆的这个对象。
    Subject subject = SecurityUtils.getSubject();
    //拿到User对象。
    User currentUser = (User) subject.getPrincipal(); 
    //设置当前用户的权限。
    info.addStringPermission(currentUser.getPerms());

    return info;
}
  1. 接下来,我们想要自定义一下未授权页面。在 ShiroConfig 的 ShiroFilterFactoryBean 中添加代码:
//未授权页面。
bean.setUnauthorizedUrl("/noauth");
  1. 在 MyController 编写对应的 controller:
@RequestMapping("/noauth")
@ResponseBody
public String unauthorized(){
    return "未经授权无法访问此页面!";
}
  1. 重启项目,再次测试,登陆用户,访问有权限的资源时可以成功访问,否则会显示"未经授权无法访问此页面!",测试成功!

14.8、Shiro和thymeleaf整合

  1. 首先,我们导入 shiro 和 thymeleaf 整合的依赖:
<!--整合 thymeleaf-shiro-->
<dependency>
    <groupId>com.github.theborakompanioni</groupId>
    <artifactId>thymeleaf-extras-shiro</artifactId>
    <version>2.0.0</version>
</dependency>
  1. 然后,在 ShiroConfig 中注入整合 thymeleaf-shiro 的 Bean:
//整合 thymeleaf-shiro:ShiroDialect
@Bean
public ShiroDialect getShiroDialect(){
    return new ShiroDialect();
}
  1. 接下来,我们想要实现如果有权限时,显示对应的按钮,没有就不显示。我们修改 index 页面的 add 和 update 标签:
<div shiro:hasPermission="user:add">
    <a th:href="@{/user/add}">add</a>
</div>

<div shiro:hasPermission="user:update">
    <a th:href="@{/user/update}">update</a>
</div>

这里还要引入 shiro 的命名空间,试了一下这两个都行:

xmlns:shiro="http://www.thymeleaf.org/thymeleaf-extras-shiro"
xmlns:shiro="http://www.w3.org/1999/xhtml
  1. 接下来,我们想要实现如果登陆了,主页后就不显示登陆按钮了。我们在 UserRealm 的认证方法中添加代码:
//给session一个值,传到前端。
Subject currentSubject = SecurityUtils.getSubject();
Session session = currentSubject.getSession();
session.setAttribute("loginUser",user);
  1. 修改 index 页面的登陆按钮:
<!--从session中判断值!-->
<div th:if="${session.loginUser==null}">
    <a th:href="@{/toLogin}">登陆</a>
</div>
  1. 启动项目测试,直接访问主页显示登陆按钮,我们登陆一下,登陆按钮消失了,而且不显示没有权限的按钮,测试成功!

完整代码

ShiroConfig:

package com.kuang.config;

import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;
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 {

    //ShiroFilterFactoryBean 3
    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager){
        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
        //设置安全管理器
        bean.setSecurityManager(defaultWebSecurityManager);

        //添加shiro的内置过滤器
        /*
        anon:无需认证就可以访问。
        authc:必须认证了才能访问。
        user:必须拥有 记住我 功能才能访问。
        perms:拥有对某个资源的权限才能访问。
        role:拥有某个角色权限才能访问。
         */
        //拦截
        Map<String, String> filterMap = new LinkedHashMap<>();

        //授权,正常情况下,没有授权会跳转到未授权页面。
        filterMap.put("/user/add","perms[user:add]");
        filterMap.put("/user/update","perms[user:update]");

        filterMap.put("/user/*","authc");
        bean.setFilterChainDefinitionMap(filterMap);

        //未授权页面。
        bean.setUnauthorizedUrl("/noauth");
        //设置登陆的请求。
        bean.setLoginUrl("/toLogin");

        return bean;
    }


    //DefaultWebSecurityManager 2
    @Bean(name = "securityManager")
    public DefaultWebSecurityManager defaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        //关联UserRealm
        securityManager.setRealm(userRealm);
        return securityManager;
    }


    //创建 realm 对象,需要自定义类。1
    @Bean
    public UserRealm userRealm(){
        return new UserRealm();
    }


    //整合 thymeleaf-shiro:ShiroDialect
    @Bean
    public ShiroDialect getShiroDialect(){
        return new ShiroDialect();
    }

}

UserRealm:

package com.kuang.config;

import com.kuang.pojo.User;
import com.kuang.service.UserService;
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.session.Session;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;
import sun.java2d.pipe.SpanShapeRenderer;

//自定义的 UserRealm ,需要继承AuthorizingRealm类。
public class UserRealm extends AuthorizingRealm {

    @Autowired
    UserService userService;

    //授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("执行了=》授权doGetAuthorizationInfo");

        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        //info.addStringPermission("user:add");
        //拿到当前登陆的这个对象。
        Subject subject = SecurityUtils.getSubject();
        User currentUser = (User) subject.getPrincipal(); //拿到User对象。
        //设置当前用户的权限。
        info.addStringPermission(currentUser.getPerms());

        return info;
    }

    //认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        System.out.println("执行了=》认证doGetAuthenticationInfo");

        //===========伪造数据库====================
/*        //用户名,密码。 数据中取
        String name = "root";
        String password = "123456";
        UsernamePasswordToken userToken = (UsernamePasswordToken) token;
        if (!userToken.getUsername().equals(name)){
            return null;//抛出异常:UnknownAccountException
        }
        //密码认证,shiro来做~
        return new SimpleAuthenticationInfo("",password,"");*/


        //===========连接真实的数据库====================
        UsernamePasswordToken userToken = (UsernamePasswordToken) token;
        User user = userService.queryUserByName(userToken.getUsername());
        if (user==null){  //没有这个人
            return null;  //UnknownAccountException
        }

        //给session一个值,传到前端。
        Subject currentSubject = SecurityUtils.getSubject();
        Session session = currentSubject.getSession();
        session.setAttribute("loginUser",user);

        //密码可以使用 MD5 进行加密。
        //密码认证,shiro来做~
        return new SimpleAuthenticationInfo(user,user.getPwd(),"");

    }

}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值