美文网首页Spring SecuritySpring-Bootspring boot
Spring-Security-教程(二)--基于数据库信息进行

Spring-Security-教程(二)--基于数据库信息进行

作者: 老亚瑟程序猿 | 来源:发表于2019-04-03 12:12 被阅读1次

    介绍

    本篇文章将讲解使用Spring Security + Mybatis + Mysql数据库实现简单的登录校验,以及密码加密校验。

    项目代码:https://github.com/Bootcap/spring-security-study-session

    一、创建数据库和需要的表结构

    1.1 创建数据库
    create database user;
    use user;
    
    1.2 创建user_info表,执行sql脚本生成
    SET NAMES utf8mb4;
    SET FOREIGN_KEY_CHECKS = 0;
    
    DROP TABLE IF EXISTS `user_info`;
    CREATE TABLE `user_info`  (
      `user_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键,自增',
      `user_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用户名',
      `user_password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用户密码',
      `user_roles` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '用户角色',
      PRIMARY KEY (`user_id`) USING BTREE
    ) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
    
    INSERT INTO `user_info` VALUES (1, '1', '1', 'ROLE_USER');
    INSERT INTO `user_info` VALUES (2, 'user', '123456', 'ROLE_USER');
    INSERT INTO `user_info` VALUES (3, 'user2', '$2a$10$RHWoRd6hPXffZemAD7Gp6ehhf929etTHVGm7JGBeJTNQIgVURnSb.', 'ROLE_USER');
    
    SET FOREIGN_KEY_CHECKS = 1;
    

    二、修改配置

    2.1 修改pom.xml

    在上篇文章所需jar包的基础上引入mysql和mybatis相关包。查看完整pom.xml

    <!-- Mybatis -->
     <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>
    </dependency>
    
    2.2 修改application.yml

    在上篇文章resources/application.yml的基础上增加"数据源配置"和"日志配置"

    # 端口号
    server:
      port: 8080
    
    spring:
      # thymeleaf配置
      thymeleaf:
        enabled: true
        encoding: UTF-8
        mode: HTML
        servlet:
          content-type: text/html
        prefix: classpath:/templates/
        suffix: .html
      # 数据源配置
      datasource:
        url: jdbc:mysql://127.0.0.1:3306/user?useUnicode=true&characterEncoding=utf8&useSSL=false # 修改为自己数据库所在ip地址
        driver-class-name: com.mysql.jdbc.Driver
        username: root # 默认用户名,修改为自己的数据库用户名
        password: # 填写自己的密码
    
    logging:
      level:
        root: INFO
        org.springframework.web: INFO
        org.springframework.security: INFO
    

    三、创建CRUD操作的相关类

    经过上面的配置修改后,还需要有一套完整的CRUD操作,本篇文章仅介绍注册和登录校验,因此只有注册和登录校验操作。

    3.1 创建数据库实体类(UserInfo.java)

    路径:com/bootcap/session/security/entity/UserInfo.java

    package com.bootcap.session.security.entity;
    
    /**
     * 数据库实体类
     * 2018-12-10 16:21
     */
    public class UserInfo {
        private Integer id;
        private String userName;
        private String password;
        private String roles;
    
        public Integer getId() {
            return id;
        }
    
        public void setId(Integer 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 getRoles() {
            return roles;
        }
    
        public void setRoles(String roles) {
            this.roles = roles;
        }
    
        @Override
        public String toString() {
            return "UserInfo{" +
                    "id=" + id +
                    ", userName='" + userName + '\'' +
                    ", password='" + password + '\'' +
                    ", roles='" + roles + '\'' +
                    '}';
        }
    }
    
    
    3.2 创建映射文件(UserInfoMapper.java)

    【提示】: 本文直接使用mybatis的注解形式进行
    sql编写

    路径:
    com/bootcap/session/security/mapper/UserInfoMapper.java

    package com.bootcap.session.security.mapper;
    
    import com.bootcap.session.security.entity.UserInfo;
    import org.apache.ibatis.annotations.*;
    
    /**
     * UserInfo映射类
     * 2018-12-10 16:26
     * 两个注解区别,网络上说是使用该@Repository注解需要配置xml映射文件,但是小编更改后并未出现异常。
     */
    // @Mapper
    @Repository
    public interface UserInfoMapper {
    
        /**
         * 新增
         * @param userInfo
         * @return
         */
        @Insert("insert into user_info(user_name,user_password,user_roles) values(#{userName},#{password}, #{roles})")
        int insert(UserInfo userInfo);
    
        /**
         * 查询
         */
        @Select("select user_name as userName,user_password as password,user_roles as roles,user_id as id from user_info where user_name = #{userName}")
        UserInfo selectByUserName(@Param("userName") String userName);
    }
    
    3.3 创建业务层类(UserInfoService.java)

    路径:com/bootcap/session/security/mapper/UserInfoService.java

    package com.bootcap.session.security.service;
    
    import com.bootcap.session.security.constant.RolesContant;
    import com.bootcap.session.security.entity.UserInfo;
    import com.bootcap.session.security.mapper.UserInfoMapper;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.stereotype.Service;
    
    /**
     * service业务层
     * 2018-12-10 16:37
     */
    @Service
    public class UserInfoService {
    
        @Autowired
        UserInfoMapper userInfoMapper;
    
        /**
         * 新增用户
         * @param userInfo
         * @return
         */
        public boolean insert(UserInfo userInfo) {
            UserInfo userInfo1 = userInfoMapper.selectByUserName(userInfo.getUserName());
            if (userInfo1 != null){
                return false;
            }
            userInfo.setRoles(RolesContant.USER);
            // 加密保存密码到数据库
            userInfo.setPassword(new BCryptPasswordEncoder().encode(userInfo.getPassword()));
            int result = userInfoMapper.insert(userInfo);
            return result == 1;
        }
    
        /**
         * 查询用户
         * @param username
         * @return
         */
        public UserInfo selectUserInfo(String username) {
            return userInfoMapper.selectByUserName(username);
        }
    
    }
    

    【说明】: 由于业务代码使用了常量类,因此需要创建一个枚举类RolesContant.java,内容如下: public static final String USER = "ROLE_USER";

    3.4 创建控制层(UserController.java)

    路径:com/bootcap/session/security/mapper/UserController.java

    package com.bootcap.session.security.controller;
    
    /**
     * 2018-12-10 17:02
     */
    @Controller
    public class UserController {
    
        @Autowired
        UserInfoService userInfoService;
    
        @PostMapping("/register")
        public String doRegister(UserInfo userInfo){
            boolean insert = userInfoService.insert(userInfo);
            if (insert){
                return "redirect:sign?success";
            }
            return "redirect:sign?error";
        }
    
        @GetMapping("/user")
        public String user(@AuthenticationPrincipal Principal principal, Model model){
            model.addAttribute("username", principal.getName());
            return "user/user";
        }
    }
    

    四、配置Spring Security获取数据库数据进行校验

    【说明】: 从流程图中我们可以得到,Spring Security最终使用UserDetailsService.loadUserByUsername()方法连接数据库获取数据并返回给校验类进行校验,因此我们需要在项目中实现该接口。

    4.1 创建MyUserDetailsService.java类并实现UsesrDetailsService
    package com.bootcap.session.security.configuration;
    
    import com.bootcap.session.security.service.UserInfoService;
    import com.bootcap.session.security.entity.UserInfo;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.core.authority.SimpleGrantedAuthority;
    import org.springframework.security.core.userdetails.User;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.core.userdetails.UsernameNotFoundException;
    import org.springframework.stereotype.Service;
    
    import java.util.ArrayList;
    import java.util.List;
    /**
     * 自定义登录校验Service
     * 2018-12-10 17:23
     */
    @Service
    public class MyUserDetailsService implements UserDetailsService {
    
        @Autowired
        UserInfoService userInfoService;
    
        @Override
        public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
            UserInfo userInfo = userInfoService.selectUserInfo(userName);
            if (userInfo == null) {
                throw new UsernameNotFoundException("用户不存在"); // 若不存在抛出用户不存在异常
            }
            // 权限字符串转化
            List<SimpleGrantedAuthority> simpleGrantedAuthorities = new ArrayList<>();
            String[] roles = userInfo.getRoles().split(",");// 获取后的Roles必须有ROLE_前缀,否则会抛Access is denied无权限异常
            for (String role : roles) {
                simpleGrantedAuthorities.add(new SimpleGrantedAuthority(role));
            }
            // 交给security进行验证并返回
            return new User(userInfo.getUserName(), userInfo.getPassword(), simpleGrantedAuthorities);
        }
    }
    
    4.2 修改WebSecurityConfig.java调用
    package com.bootcap.session.security.configuration;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Configuration;
    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;
    
    /**
     * 2018-12-10 11:03
     */
    @Configuration
    @EnableWebSecurity
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
    
        @Autowired
        MyUserDetailsService myUserDetailsService;
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests()
                    .antMatchers("/","/index").permitAll() // permitAll被允许访问
                    .antMatchers("/user/**").hasRole("USER")// 指定所有user页面需要USER角色才能访问
                .and()
                    .formLogin().loginPage("/login").defaultSuccessUrl("/user")
                .and()
                    .logout().logoutUrl("/logout").logoutSuccessUrl("/login");
        }
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    //        auth.inMemoryAuthentication() // 在内存中进行身份验证
    //                .passwordEncoder(new BCryptPasswordEncoder())
    //                .withUser("user")
    //                .password(new BCryptPasswordEncoder().encode("123456"))
    //                .roles("USER");
    
    // 修改的地方,将上篇文章的内存验证改为获取数据库,并使用了密码加密         
    auth.userDetailsService(myUserDetailsService).passwordEncoder(new BCryptPasswordEncoder());
        }
    
    }
    
    扩展:对密码进行加密

    1、修改WebSecurityConfig.configure()方法

     auth.userDetailsService(myUserDetailsService).passwordEncoder(new BCryptPasswordEncoder());
    

    2、在UserInfoService.insert()方法对password字段进行加密存储

     public boolean insert(UserInfo userInfo) {
            UserInfo userInfo1 = userInfoMapper.selectByUserName(userInfo.getUserName());
            if (userInfo1 != null){
                return false;
            }
            userInfo.setRoles(RolesContant.USER);
            userInfo.setPassword(new BCryptPasswordEncoder().encode(userInfo.getPassword()));
            int result = userInfoMapper.insert(userInfo);
            return result == 1;
        }
    
    4.3 修改TemplateConfig.java(用于新增的注册页面)
    @Configuration
    public class TemplateConfig implements WebMvcConfigurer {
    
        @Override
        public void addViewControllers(ViewControllerRegistry registry) {
            registry.addViewController("/").setViewName("index");
            registry.addViewController("/index").setViewName("index");
            registry.addViewController("/hello").setViewName("hello");
            registry.addViewController("/login").setViewName("login");
            // 新增注册页面
           registry.addViewController("/sign").setViewName("register");
        }
    }
    
    4.4 上述配置完成后,我们需要增加和修改几个页面用于测试

    4.4.1 修改login.html(增加跳转到注册连接)

    <!DOCTYPE html>
    <html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>登录页面</title>
    </head>
    <body>
    <h1>登录页面</h1>
    <div th:if="${param.error}">
        用户名或密码不正确
    </div>
    <div th:if="${param.logout}">
        你已经退出登录
    </div>
    <form th:action="@{/login}" method="post">
        <div><label> 用户名: <input type="text" name="username"/> </label></div>
        <div><label> 密&nbsp;&nbsp;&nbsp;码: <input type="password" name="password"/> </label></div>
        <div>
            <input type="submit" value="登录"/>
            <a th:href="@{/sign}">注册</a>
        </div>
    </form>
    </body>
    </html>
    

    4.4.2 新增register.html注册页面

    <html xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>注册</title>
    </head>
    <body>
    <div class="panel-body">
        <h1>注册页面</h1>
        <form th:action="@{/register}" method="post">
            <div>
                账号: <input type="text" name="userName" id="username" placeholder="账号">
            </div>
            <div>
                密码: <input type="password" class="form-control" name="password" id="password" placeholder="密码">
            </div>
            <br>
            <div th:if="${param.error}">
                <p>注册失败,账号已存在!</p>
            </div>
            <div th:if="${param.success}">
                <p>注册成功,可以登录了!</p>
            </div>
            <div>
                <button type="submit" class="btn btn-primary btn-block">注册</button>
                <a href="/login">返回登录页面</a>
            </div>
        </form>
    </div>
    
    </body>
    
    </html>
    

    4.4.3 新增user.html(用于登录后跳转的页面)
    路径:templates/user/user.html

    <html xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>用户界面</title>
    </head>
    
    <body>
    
    <div class="container" style="margin-top: 60px">
    
        <div style="text-align: center; margin-top: 10%">
            <p th:text="${username}" style="margin-top: 25px; font-size: 20px; color: crimson"></p>
            <form th:action="@{/logout}" method="post">
                <button style="margin-top: 20px">退出登录</button>
            </form>
        </div>
    
    </div>
    </body>
    </html>
    

    五、启动运行

    至此,基本修改已经完毕,这时候可以启动Application.java启动运行了?答案:不能!!!

    【提示】: 本项目的启动类是在app包下,因此我们配置的mapper映射文件的@Repository注解为mybatis的注解,所以boot不会自动注入,因此需要加入@MapperScan(basePackages = "com.bootcap.session.security.mapper")

    package com.bootcap.session.security.app;
    
    /**
     * 启动类
     * 2018-12-10 11:03
     */
    @SpringBootApplication
    @ComponentScan(basePackages = {"com.bootcap.session.security"})
    @MapperScan(basePackages = "com.bootcap.session.security.mapper")
    public class Application {
        public static void main(String[] args) {
            SpringApplication.run(Application.class,args);
        }
    }
    

    六、测试结果

    6.1 项目启动后,浏览器访问:localhost:8080/user,会发现直接跳转到登录页


    6.2 目前我们没有账号,点击注册按钮进行注册,注册好后。我们使用注册的账号进行登录 ,可以正常进入user页面


    七、扩展篇 - 自定义Filter过滤器

    7.1 创建两个自定义Filter过滤器(BeforeFilter.java和BeforeFilter.java)

    路径:com/bootcap/session/security/filter

    package com.bootcap.session.security.filter;
    
    /**
     * BeforeFilter
     * 2018-12-12 10:44
     */
    public class BeforeFilter extends GenericFilterBean {
        @Override
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
            System.out.println("在 UsernamePasswordAuthenticationFilter 前添加 BeforeLoginFilter");
            // 继续调用 Filter 链
            filterChain.doFilter(servletRequest, servletResponse);
        }
    }
    
    package com.bootcap.session.security.filter;
    /**
     * AfterCsrfFilter
     * 2018-12-12 10:46
     */
    public class AfterCsrfFilter extends GenericFilterBean {
        @Override
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
            System.out.println("在 CsrfFilter 后添加 AfterCsrfFilter");
            filterChain.doFilter(servletRequest,servletResponse);
        }
    }
    
    7.2 在WebSecurityConfig.configure()方法中加入调用
    @Override
        protected void configure(HttpSecurity http) throws Exception {
             ... 省略部分代码 ...
            // 在 UsernamePasswordAuthenticationFilter 前添加 BeforeLoginFilter
            http.addFilterBefore(new BeforeFilter(), UsernamePasswordAuthenticationFilter.class);
    
            // 在 CsrfFilter 后添加 AfterCsrfFilter
            http.addFilterAfter(new AfterCsrfFilter(), CsrfFilter.class);
        }
    
    7.3 运行程序并访问任意可访问页面,我们能够的到两个过滤器的执行顺序
    2018-12-12 10:49:34.607  INFO 1060 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring FrameworkServlet 'dispatcherServlet'
    2018-12-12 10:49:34.607  INFO 1060 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization started
    2018-12-12 10:49:34.611  INFO 1060 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization completed in 4 ms
    在 CsrfFilter 后添加 AfterCsrfFilter
    在 UsernamePasswordAuthenticationFilter 前添加 BeforeLoginFilter
    在 CsrfFilter 后添加 AfterCsrfFilter
    在 UsernamePasswordAuthenticationFilter 前添加 BeforeLoginFilter
    

    上一篇:Spring Security 入门教程(一) - 简单的登录认证
    下一篇:Spring Security 入门教程(三)- 基于登录认证记住我实例

    相关文章

      网友评论

        本文标题:Spring-Security-教程(二)--基于数据库信息进行

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