美文网首页程序员
从零开始构建springboot 2.x Web项目【持续更新】

从零开始构建springboot 2.x Web项目【持续更新】

作者: 谢随安 | 来源:发表于2018-12-04 17:22 被阅读5次

    简介

    文章内容介绍:基于SpringBoot 2.x 的demo,集成了 spring-boot-security、mybatis、druid、redis 等等

    读者按需自取,还有很多未完成的,慢慢来

    项目代码下载地址:https://github.com/ChaselX/spring-boot-2-demo

    使用Maven构建项目

    可以用IDEA集成好的Spring Initializr来创建一个SpringBoot项目。

    IDEA创建SpringBoot项目 Group和Artifact根据你的项目随意命名

    勾选上自己需要的依赖(不选也没关系,在Maven中手动加即可)

    Spring Boot应用启动器

    Spring Boot提供了很多应用启动器,分别用来支持不同的功能,因为Spring Boot的自动化配置特性,我们不需考虑项目依赖版本问题,使用Spring Boot的应用启动器,它能自动帮我们将相关的依赖全部导入到项目中。

    这里介绍几个常见的应用启动器:

    • spring-boot-starter: Spring Boot的核心启动器,包含了自动配置、日志和YAML
    • spring-boot-starter-aop: 支持AOP面向切面编程的功能,包括spring-aop和AspecJ
    • spring-boot-starter-cache: 支持Spring的Cache抽象
    • spring-boot-starter-artermis: 通过Apache Artemis支持JMS(Java Message Service)的API
    • spring-boot-starter-data-jpa: 支持JPA
    • spring-boot-starter-data-solr: 支持Apache Solr搜索平台,包括spring-data-solr
    • spring-boot-starter-freemarker: 支持FreeMarker模板引擎
    • spring-boot-starter-jdbc: 支持JDBC数据库
    • spring-boot-starter-Redis: 支持Redis键值储存数据库,包括spring-redis
    • spring-boot-starter-security: 支持spring-security
    • spring-boot-starter-thymeleaf: 支持Thymeleaf模板引擎,包括与Spring的集成
    • spring-boot-starter-web: 支持全栈式web开发,包括tomcat和Spring-WebMVC
    • spring-boot-starter-log4j: 支持Log4J日志框架
    • spring-boot-starter-logging: 引入Spring Boot默认的日志框架Logback

    也可以在Spring官网 https://start.spring.io/ 构建项目,勾选上自己需要的依赖即可(之后在Maven里再加也可以)。

    项目的创建成功以后的Maven如下所示:

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
    
        <groupId>com.example</groupId>
        <artifactId>demo</artifactId>
        <version>0.0.1-SNAPSHOT</version> 发布文章
        <packaging>jar</packaging>
    
        <name>demo</name>
        <description>Demo project for Spring Boot</description>
    
        <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>2.0.5.RELEASE</version>
            <relativePath/> <!-- lookup parent from repository -->
        </parent>
    
        <properties>
            <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
            <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
            <java.version>1.8</java.version>
        </properties>
    
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-jdbc</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
                <version>1.3.2</version>
            </dependency>
    
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <scope>runtime</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
            </dependency>
        </dependencies>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                </plugin>
            </plugins>
        </build>
    </project>
    

    配置系统基本参数

    要访问mysql数据库,还需要配置一下系统变量。
    默认的系统变量配置文件是项目当前文件夹的/src/main/resources下的application.properties

    但是我更喜欢yml的风格,删掉这个文件,在相同的位置创建一个application.yml文件

    # 指定端口号
    server:
      port: 8080
    spring:
      datasource:
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:mysql://127.0.0.1:3306/demo?useUnicode=true&characterEncoding=utf-8&useSSL=false
        username: root
        password:
    mybatis:
      mapper-locations: classpath*:mapper/*.xml  #注意:一定要对应mapper映射xml文件的所在路径
      type-aliases-package: com.example.demo.model.entity  # 注意:对应实体类的路径
    

    编写控制层处理HTTP请求

    /src/main/java/com/example/demo/controller下创建一个HelloController.java

    package com.example.demo.controller;
    
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    /**
     * @author ChaselX
     * @date 2018/10/6 18:56
     */
    @RestController
    @RequestMapping("/")
    public class HelloController {
        @GetMapping
        public String sayHello() {
            return "Hello SpringBoot!";
        }
    }
    

    运行DemoApplication.java,浏览器请求localhost:8080可以看到如下效果

    localhost:8080

    通过Mybatis操作数据库

    根据之前的配置

    mybatis:
      mapper-locations: classpath*:mapper/*.xml  # 注意:一定要对应mapper映射xml文件的所在路径
      type-aliases-package: com.example.demo.model.entity # 对应实体类的路径
    

    /src/main/java/com/example/demo/model/entity下创建实体类SysUser.java

    package com.example.demo.model.entity;
    
    /**
     * @author ChaselX
     * @date 2018/10/7 17:42
     */
    public class SysUser {
    
        private static final long serialVersionUID = 215517484123587L;
    
        /**
         * 主键id
         */
        private Long id;
    
        /**
         * 账号
         */
        private String username;
    
        /**
         * 密码
         */
        private String password;
    
        /**
         * 姓名
         */
        private String name;
    
        /**
         * 电话号码
         */
        private String mobile;
    
        /**
         * 账号是否可用
         */
        private boolean enabled;
    
    
        public Long getId() {
            return id;
        }
    
        public void setId(Long id) {
            this.id = id;
        }
    
        public String getUsername() {
            return username;
        }
    
        public void setUsername(String username) {
            this.username = username;
        }
    
        public String getPassword() {
            return password;
        }
    
        public void setPassword(String password) {
            this.password = password;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public String getMobile() {
            return mobile;
        }
    
        public void setMobile(String mobile) {
            this.mobile = mobile;
        }
    
        public boolean isEnabled() {
            return enabled;
        }
    
        public void setEnabled(boolean enabled) {
            this.enabled = enabled;
        }
    }
    

    /src/main/java/com/example/demo/mapper下创建UserMapper.java

    package com.example.demo.mapper;
    
    import com.example.demo.model.entity.SysUser;
    import org.apache.ibatis.annotations.Insert;
    import org.apache.ibatis.annotations.Select;
    
    import java.util.List;
    
    /**
     * @author ChaselX
     * @date 2018/10/7 17:53
     */
    public interface UserMapper {
        @Insert("INSERT INTO user(username, password, name, mobile) VALUES(#{username}, #{password}, #{name}, #{mobile})")
    //    返回插入记录的主键id
    //    @SelectKey(statement = "select LAST_INSERT_ID()", keyProperty = "id", before = false, resultType = Long.class)
        int add(SysUser user);
    
        @Select("SELECT * from user")
        List<SysUser> getAll();
    }
    

    DemoApplication.java上加一个@MapperScan("com.example.demo.mapper")注解,这个注解的作用是自动扫描com.example.demo.mapper包下的Mapper,实现并注入到Bean中。

    package com.example.demo;
    
    import org.mybatis.spring.annotation.MapperScan;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    
    @SpringBootApplication
    @MapperScan("com.example.demo.mapper")
    public class DemoApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(DemoApplication.class, args);
        }
    }
    
    

    编写对应的后端控制层与服务层业务逻辑代码,文件位置参考代码中的package

    package com.example.demo.service;
    
    import com.example.demo.model.entity.SysUser;
    
    import java.util.List;
    
    /**
     * @author ChaselX
     * @date 2018/10/8 8:59
     */
    public interface UserService {
        List<User> getAll();
    
        boolean addUser(SysUser user);
    }
    
    package com.example.demo.service.impl;
    
    import com.example.demo.mapper.UserMapper;
    import com.example.demo.model.entity.SysUser;
    import com.example.demo.service.UserService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    import java.util.List;
    
    /**
     * @author ChaselX
     * @date 2018/10/8 8:59
     */
    @Service
    public class UserServiceImpl implements UserService {
        @Autowired
        private UserMapper userMapper;
    
        @Override
        public List<SysUser> getAll() {
            return userMapper.getAll();
        }
    
        @Override
        public boolean addUser(SysUser user) {
            return userMapper.add(user) > 0;
        }
    }
    
    package com.example.demo.controller;
    
    import com.example.demo.model.entity.SysUser;
    import com.example.demo.service.UserService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.*;
    
    /**
     * @author ChaselX
     * @date 2018/10/8 9:08
     */
    @RestController
    @RequestMapping("/users")
    public class UserController {
        @Autowired
        private UserService userService;
    
        @GetMapping
        public ResponseEntity getAllUsers() {
            return new ResponseEntity<>(userService.getAll(), HttpStatus.OK);
        }
    
        @PostMapping
        public ResponseEntity addUser(@RequestBody SysUser user) {
            if (userService.addUser(user)) {
                return ResponseEntity.ok().build();
            }
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("创建用户失败!");
        }
    }
    

    现在,可以运行项目利用postman对上面的功能接口进行测试了

    redis配置与使用(非必须)

    这里不讲redis的安装和启动,只讲项目如何使用redis

    pom.xml中加入spring-boot-starter-data-redis依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
    

    对系统配置文件application.yml做如下配置

    spring: 
      redis:
          host: 127.0.0.1
          port: 6379
          timeout: 2000ms
          database: 0
          password:
          lettuce:
            pool:
              max-active:  100 # 连接池最大连接数(使用负值表示没有限制)
              max-idle: 100 # 连接池中的最大空闲连接
              min-idle: 50 # 连接池中的最小空闲连接
              max-wait: 6000ms
    

    由于使用的是SpringBoot 2.0推荐的lettuce连接池。SpringBoot 2.0需要手动构建LettuceConnectionFactory Bean

    package com.example.demo.common.config;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
    import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
    
    /**
     * @author ChaselX
     * @date 2018/9/4 10:02
     */
    @Configuration
    public class RedisConfig {
        @Autowired
        private RedisProperties redisProperties;
    
        @Bean
        public LettuceConnectionFactory lettuceConnectionFactory() {
            return new LettuceConnectionFactory(new RedisStandaloneConfiguration(redisProperties.getHost(), redisProperties.getPort()));
        }
    }
    
    Redis哨兵主从模式

    对系统配置文件application.yml做如下配置

      spring: 
        redis:
          timeout: 2000ms
          database: 1
          lettuce:
            pool:
              max-active:  100 # 连接池最大连接数(使用负值表示没有限制)
              max-idle: 100 # 连接池中的最大空闲连接
              min-idle: 50 # 连接池中的最小空闲连接
              max-wait: 6000ms
          sentinel:
            master: mymaster
            nodes: 10.1.58.117:27379,10.1.58.137:27379
          password: 
    

    构建哨兵模式的连接工厂,修改LettuceConnectionFactory

        @Bean
        public LettuceConnectionFactory redisConnectionFactory() {
            RedisSentinelConfiguration redisSentinelConfiguration = new RedisSentinelConfiguration(redisProperties.getSentinel().getMaster(), new HashSet<>(redisProperties.getSentinel().getNodes()));
            redisSentinelConfiguration.setPassword(RedisPassword.of(redisProperties.getPassword()));
            return new LettuceConnectionFactory(redisSentinelConfiguration);
        }
    
    Redis集群

    对系统配置文件application.yml做如下配置

    spring: 
      redis:
        cluster:
          nodes: 
            - 192.168.1.111:7001
            - 192.168.1.112:7001
            - 192.168.1.110:7002
            - 192.168.1.110:7001
            - 192.168.1.111:7002
            - 192.168.1.112:7001
          password:
          lettuce:
            pool:
              max-active:  100 # 连接池最大连接数(使用负值表示没有限制)
              max-idle: 100 # 连接池中的最大空闲连接
              min-idle: 50 # 连接池中的最小空闲连接
              max-wait: 6000ms
          timeout: 2000ms
    

    构建集群模式的连接工厂,修改LettuceConnectionFactory

        @Bean
        public LettuceConnectionFactory redisConnectionFactory(RedisClusterConfiguration redisClusterConfiguration) {
            return new LettuceConnectionFactory(redisClusterConfiguration);
        }
    
        @Bean
        public RedisClusterConfiguration redisClusterConfiguration() {
            RedisClusterConfiguration configuration = new RedisClusterConfiguration(redisProperties.getCluster().getNodes());
            configuration.setPassword(RedisPassword.of(redisProperties.getPassword()));
            return configuration;
        }
    
    使用Redis

    改造之前的HelloController对redis功能做简单的测试

    package com.example.demo.controller;
    
    import org.mybatis.spring.annotation.MapperScan;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    import java.util.concurrent.TimeUnit;
    
    /**
     * @author ChaselX
     * @date 2018/10/6 18:56
     */
    @RestController
    @RequestMapping("/")
    @MapperScan("com.example.demo.mapper")
    public class HelloController {
        @Autowired
        private RedisTemplate<String, String> stringStringRedisTemplate;
    
        @GetMapping
        public String sayHello() {
            stringStringRedisTemplate.opsForValue().set("Say hello", "Hello SpringBoot From Redis!", 5, TimeUnit.SECONDS);
            return stringStringRedisTemplate.opsForValue().get("Say hello");
        }
    }
    

    运行项目

    数据库创建用户及授权(非必须)

    对于正式生产环境,你登录到数据库的往往不会是root用户,而是通过仅具有特定数据库权限的用户登录数据库。可以通过下面的SQL语句创建数据库用户。

    insert into mysql.user(Host,User,Password) values("%","admin",password("admin123"));
    
    GRANT ALL ON db_name.* TO admin@% identified by "admin123";
    
    flush privileges;
    

    数据库连接池druid配置(非必须)

    Maven中加入druid依赖

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.10</version>
        </dependency>
    

    application.yml中加上如下配置

    spring:
      datasource:
        druid:
          # 初始化大小,最小,最大
          initialSize: 5
          minIdle: 5
          maxActive: 20
          # 配置获取连接等待超时的时间
          maxWait: 60000
          # 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
          filters: stat,wall,slf4j
          # 打开PSCache,并且指定每个连接上PSCache的大小
          poolPreparedStatements: true
          maxOpenPreparedStatements: 20
          validationQuery: SELECT 1 FROM DUAL
          testWhileIdle: true
          testOnBorrow: false
          testOnReturn: false
          timeBetweenEvictionRunsMillis: 60000
          minEvictableIdleTimeMillis: 300000
          # yml方式配置servlet与filter
          stat-view-servlet:
            enabled: true
            # /druid登录账号
            login-username: admin 
            # /druid登录密码
            login-password: admin 
            reset-enable: false
          web-stat-filter:
            enabled: true
            exclusions: /druid/*,*.js,*.gif,*.jpg,*.png,*.css,*.ico
            url-pattern: /*
    

    配置好druid后访问 {url}/druid,由于配置了登录账号和密码,需要身份认证

    认证成功后便可通过监控页面查看各项监控数据

    基于Spring Security安全框架的的认证与验证

    传统的登录是通过cookie-session方式实现登录认证,而在前后端分离的情况下,实现用户鉴权的更好的方式是使用JWT(Java Web Token)

    引入Spring Security

    pom.xml中加入Spring Security依赖

            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-security</artifactId>
            </dependency>
    

    在项目中加入WebSecurityConfig配置文件

    package com.example.demo.common.config.security;
    
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.builders.WebSecurity;
    import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    
    /**
     * @author ChaselX
     * @date 2018/11/28 16:18
     */
    @Configuration
    @EnableWebSecurity
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
        // http请求安全配置
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.csrf().disable().authorizeRequests()
                    .antMatchers("/").permitAll()
                    .anyRequest().authenticated() // 所有请求都需要权限验证
                    .and()
                    .logout().permitAll()
                    .and()
                    .formLogin();
    
        }
    
    //    // 忽略web静态资源,若需要
    //    @Override
    //    public void configure(WebSecurity web) throws Exception {
    //        web.ignoring().antMatchers("/js/**", "/css/**", "/images/**");
    //    }
    }
    

    修改一下之前的HelloController

    package com.example.demo.controller;
    
    import org.mybatis.spring.annotation.MapperScan;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    import java.util.concurrent.TimeUnit;
    
    /**
     * @author ChaselX
     * @date 2018/10/6 18:56
     */
    @RestController
    @RequestMapping("/")
    @MapperScan("com.example.demo.mapper")
    public class HelloController {
        @Autowired
        private RedisTemplate<String, String> stringStringRedisTemplate;
    
        @GetMapping
        public String mainPage() {
            return "Hello SpringBoot From \"/\"";
        }
    
        @GetMapping("/sayHello")
        public String sayHello() {
            stringStringRedisTemplate.opsForValue().set("demo:SayHello", "Hello SpringBoot From Redis!", 5, TimeUnit.SECONDS);
            return stringStringRedisTemplate.opsForValue().get("demo:SayHello");
        }
    }
    

    运行项目测试,访问系统首页

    首页正常展示

    访问localhost:8080/sayHello会跳转到http://localhost:8080/login登录页

    Spring Security的安全策略已经生效,但是具体的登录功能还没有实现

    基于Spring Security的登录功能实现

    要实现基于Spring Security的登录功能,首先需要定义一个继承了Spring Security的UserDetailsService接口的接口,修改一下之前的UserService

    package com.example.demo.service;
    
    import com.example.demo.model.entity.SysUser;
    import org.springframework.security.core.userdetails.UserDetailsService;
    
    import java.util.List;
    
    /**
     * @author ChaselX
     * @date 2018/10/8 8:59
     */
    public interface UserService extends UserDetailsService {
        List<SysUser> getAll();
    
        boolean addUser(SysUser user);
    }
    

    再修改一下接口的实现类UserServiceImpl实现UserDetailsService接口的loadUserByUsername()方法

        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            return null;
        }
    

    因为要返回UserDetails对象,具体方法实现先放在一边。先看看如何使用这个方法进行用户认证。在SpringSecurityConfig加入以下代码

        @Autowired
        private UserDetailsService userServiceImpl; // 属性名为userServiceImpl对应实现类的名称
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(userServiceImpl);
        }
    
    密码自定义加密验证

    用户的密码在数据库中通常是以密文的形式存储的,为此需要实现一个密码的自定义验证,指定Spring Security使用什么加密规则对密码进行验证,创建一个beanbCryptPasswordEncoder

    package com.example.demo.common.config.security;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    
    /**
     * @author ChaselX
     * @date 2018/11/28 19:21
     */
    @Configuration
    public class BaseConfig {
        @Bean
        public BCryptPasswordEncoder bCryptPasswordEncoder() {
            return new BCryptPasswordEncoder();
        }
    }
    

    修改WebSecurityConfig的代码指定passwordEncoder

        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(userServiceImpl).passwordEncoder(bCryptPasswordEncoder);
        }
    

    由于使用了密码加密验证,需要修改一下添加用户那里的逻辑,在插入数据库之前先对密码做BCrypt加密

    package com.example.demo.service.impl;
    
    import com.example.demo.mapper.UserMapper;
    import com.example.demo.model.entity.SysUser;
    import com.example.demo.service.UserService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UsernameNotFoundException;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.stereotype.Service;
    
    import java.util.ArrayList;
    import java.util.List;
    
    /**
     * @author ChaselX
     * @date 2018/10/8 8:59
     */
    @Service
    public class UserServiceImpl implements UserService {
        @Autowired
        private UserMapper userMapper;
    
        @Autowired
        private BCryptPasswordEncoder encoder;
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            return null;
        }
    
        @Override
        public List<SysUser> getAll() {
            return userMapper.getAll();
        }
    
        @Override
        public boolean addUser(SysUser sysUser) {
            sysUser.setPassword(encoder.encode(sysUser.getPassword()));
            return userMapper.add(sysUser) > 0;
        }
    }
    

    这样便实现了密码的自定义验证

    基于RBAC的用户、角色、权限

    虽然实现了加密验证,但是却没有定义权限验证相关的用户、角色、权限

    要使用Spring Security安全框架,由于UserDetailsService.loadUserByUsername()返回的是一个UserDetails类型的对象。UserDetails接口中最重要的是getAuthorities()方法,用户所具有的所有权限都定义在里面。因此需要做些处理,从数据库获取系统用户,并根据相关的角色权限来构造UserDetailsauthorities属性,为此首先需要定义好系统的用户、角色、权限实体表与它们之间的关系表。用户实体类已经定义好,还有角色、权限、用户角色关系以及角色权限关系未定义。

    package com.example.demo.model.entity;
    
    import java.util.Date;
    
    /**
     * 角色实体类
     *
     * @author ChaselX
     * @date 2018/12/1 16:05
     */
    public class Role {
        /**
         * 角色的authority前缀
         */
        public static final String PREFIX = "ROLE_";
    
        /**
         * 主键id
         */
        private Long id;
    
        /**
         * 角色代号
         */
        private String code;
    
        /**
         * 角色名
         */
        private String name;
    
        /**
         * 备注
         */
        private String remark;
    
        private Long operator;
    
        private Date operateTime;
    
        // 省略get/set方法代码
    }
    
    package com.example.demo.model.entity;
    
    import java.util.Date;
    
    /**
     * 权限实体类
     * 
     * @author ChaselX
     * @date 2018/12/1 16:00
     */
    public class Permission {
        /**
         * 主键id
         */
        private Long id;
    
        /**
         * 权限编码
         */
        private String code;
    
        /**
         * 权限名称
         */
        private String name;
    
        /**
         * 操作人
         */
        private String operator;
    
        /**
         * 操作时间
         */
        private Date operateTime;
    
        // 省略get/set方法代码
    }
    
    
    package com.example.demo.model.entity;
    
    import java.util.Date;
    
    /**
     * 用户-角色关系表
     *
     * @author ChaselX
     * @date 2018/12/1 16:17
     */
    public class UserRole {
        private Long id;
    
        private Long userId;
    
        private Long roleId;
    
        private Long operator;
    
        private Date operateTime;
    
        // 省略get/set方法代码
    }
    
    package com.example.demo.model.entity;
    
    import java.util.Date;
    
    /**
     * 角色-权限关系表
     *
     * @author ChaselX
     * @date 2018/12/1 16:25
     */
    public class RolePermission {
        private Long id;
    
        private Long roleId;
    
        private Long permissionId;
    
        private Long operator;
    
        private Date operateTime;
    
        // 省略get/set方法代码
    }
    

    数据库相关建表这边就不赘述了,按照基本的主键id自增,参数驼峰命名法转下划线命名法即可,若有需要日后再补充。

    为了减少数据库的访问次数,一次性将用户相关的信息(角色、权限)查询出来,封装一个SysUserVO

    package com.example.demo.model.vo;
    
    import com.example.demo.model.entity.Permission;
    import com.example.demo.model.entity.Role;
    import com.example.demo.model.entity.SysUser;
    
    import java.util.List;
    
    /**
     * @author ChaselX
     * @date 2018/12/1 16:51
     */
    public class SysUserVO extends SysUser {
        private List<Role> roles;
    
        private List<Permission> permissions;
    
        public List<Role> getRoles() {
            return roles;
        }
    
        public void setRoles(List<Role> roles) {
            this.roles = roles;
        }
    
        public List<Permission> getPermissions() {
            return permissions;
        }
    
        public void setPermissions(List<Permission> permissions) {
            this.permissions = permissions;
        }
    }
    

    UserMapper添加一个查询用户详细信息的方法getDetailsByUsername

    package com.example.demo.mapper;
    
    import com.example.demo.model.entity.SysUser;
    import com.example.demo.model.vo.SysUserVO;
    import org.apache.ibatis.annotations.*;
    
    import java.util.List;
    
    /**
     * @author ChaselX
     * @date 2018/10/7 17:53
     */
    public interface UserMapper {
        @Insert("INSERT INTO user(username, password, name, mobile) VALUES(#{username}, #{password}, #{name}, #{mobile})")
    //    返回插入记录的主键id
    //    @SelectKey(statement = "select LAST_INSERT_ID()", keyProperty = "id", before = false, resultType = Long.class)
        int add(SysUser sysUser);
    
        @Select("SELECT * from user")
        List<SysUser> getAll();
    
        @Select("SELECT * FROM user WHERE username = #{username}")
        @Results({
                @Result(property = "roles", column = "user_id", many = @Many(select = "com.example.demo.mapper.RoleMapper.getRolesByUserId")),
                @Result(property = "permissions", column = "user_id", many = @Many(select = "com.example.demo.mapper.PermissionMapper.getPermissionsByUserId"))
        })
        SysUserVO getDetailsByUsername(String username);
    }
    

    注意,由于SysUserVO的两个字段roles与permissions是集合类型的,所以用到了@Results@Result@Many注解,更全面的说明可参考mybatis官方文档(需翻墙)

    @Many注解的select属性表明select引用的来源分别为com.example.demo.mapper.RoleMapper下的getRolesByUserId方法与com.example.demo.mapper.PermissionMapper下的getPermissionsByUserId方法

    package com.example.demo.mapper;
    
    import com.example.demo.model.entity.Role;
    import org.apache.ibatis.annotations.Select;
    
    import java.util.List;
    
    /**
     * @author ChaselX
     * @date 2018/12/4 14:36
     */
    public interface RoleMapper {
        @Select("select r.id, r.code, r.name, r.remark from role r where r.id in (select ur.role_id from user_role ur where ur.user_id = #{userId})")
        List<Role> getRolesByUserId(Long userId);
    }
    
    package com.example.demo.mapper;
    
    import com.example.demo.model.entity.Permission;
    import org.apache.ibatis.annotations.Select;
    
    import java.util.List;
    
    /**
     * @author ChaselX
     * @date 2018/12/4 14:58
     */
    public interface PermissionMapper {
        @Select("SELECT p.id, p.code, p.name FROM permission p WHERE p.id IN (" +
                "SELECT rp.permission_id FROM role_permission rp WHERE rp.role_id in(" +
                "SELECT ur.role_id FROM user_role ur WHERE ur.user_id = #{userId}))")
        List<Permission> getPermissionsByUserId(Long userId);
    }
    

    这些都完成了以后就可以动手实现前面放置在一边的UserServiceImplloadUserByUsername方法了

        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            SysUserVO userVO = userMapper.getDetailsByUsername(username);
            List<GrantedAuthority> authorities = new ArrayList<>();
            for (Role role : userVO.getRoles()
            ) {
                authorities.add(new SimpleGrantedAuthority(Role.PREFIX + role.getCode()));
            }
            for (Permission permission : userVO.getPermissions()
            ) {
                authorities.add(new SimpleGrantedAuthority(permission.getCode()));
            }
            return new User(userVO.getUsername(), userVO.getPassword(), authorities);
        }
    

    现在登录功能已经实现,可以运行项目进行登录功能测试了(tips:在登录之前需要先在用户表中加入使用BCrypt加密的用户记录),项目启动后访问http://localhost:8080/login会跳转到登录界面

    输入用户名和密码,登录成功后会返回系统首页


    未完待续 未完待续 未完待续 未完待续 未完待续 未完待续 未完待续 未完待续 未完待续

    aes加密传输登录密码

    在非https的情况下,若无特殊处理,用户的登录密码会以明文的方式传输给后端。因此需要对用户密码进行加密传输,保证请求报文即使被截取,也不会泄露用户的密码。前后端加解密流程如下(图片引用):

    调用接口获取动态加密秘钥

    在客户端向后端post登录信息之前,先调用接口获取动态加密秘钥,前端生成随机秘钥,后端会把缓存放进redis里,为了安全性考虑,缓存的有效期设置为5s

    客户端收到动态加密秘钥后,通过秘钥对密码做AES加密,将登陆信息通过POST请求发送给后端

    jwt动态刷新

    数据库分页查询

    往项目的pom.xml里加入

    <dependency>
        <groupId>com.github.pagehelper</groupId>
        <artifactId>pagehelper</artifactId>
        <version>latest version</version>
    </dependency>
    

    相关文章

      网友评论

        本文标题:从零开始构建springboot 2.x Web项目【持续更新】

        本文链接:https://www.haomeiwen.com/subject/unkhaftx.html