美文网首页Spring-Boot程序员Spring Security
【SpringSecurity系列01】初识SpringSecu

【SpringSecurity系列01】初识SpringSecu

作者: 余空啊 | 来源:发表于2019-04-11 19:26 被阅读9次

    ​ 什么是SpringSecurity

    ​ Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。

    以上来介绍来自wiki,比较官方。

    ​ 用自己的话 简单介绍一下,Spring Security基于 Servlet 过滤器链的形式,为我们的web项目提供认证授权服务。它来自于Spring,那么它与SpringBoot整合开发有着天然的优势,目前与SpringSecurity对应的开源框架还有shiro。接下来我将通过一个简单的例子带大家来认识SpringSecurity,然后通过分析它的源码带大家来认识一下SpringSecurity是如何工作,从一个简单例子入门,大家由浅入深的了解学习SpringSecurity

    通常大家在做一个后台管理的系统的时候,应该采用session判断用户是否登录。我记得我在没有接触学习SpringSecurity与shiro之前。对于用户登录功能实现通常是如下:

    public String login(User user, HttpSession session){
      //1、根据用户名或者id从数据库读取数据库中用户
      //2、判断密码是否一致
        //3、如果密码一致
            session.setAttribute("user",user);
        //4、否则返回登录页面
      
    }
    
    对于之后那些需要登录之后才能访问的url,通过SpringMvc的拦截器中的#preHandle来判断session中是否有user对象
    如果没有 则返回登录页面
    如果有, 则允许访问这个页面。
    

    但是在SpringSecurity中,这一些逻辑已经被封装起来,我们只需要简单的配置一下就能使用。

    接下来我通过一个简单例子大家认识一下SpringSecurity

    本文基于SpringBoot,如果大家对SpringBoot不熟悉的话可以看看我之前写的SpringBoot入门系列

    我使用的是:

    • SpringBoot 2.1.4.RELEASE
    • SpringSecurity 5
    <?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>
        <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>2.1.4.RELEASE</version>
            <relativePath/> <!-- lookup parent from repository -->
        </parent>
        <groupId>com.yukong</groupId>
        <artifactId>springboot-springsecurity</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <name>springboot-springsecurity</name>
        <description>springboot-springsecurity study</description>
    
        <properties>
            <java.version>1.8</java.version>
        </properties>
    
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-security</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>2.0.1</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>
            <dependency>
                <groupId>org.springframework.security</groupId>
                <artifactId>spring-security-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>
    
    

    配置一下数据库 以及MyBatis

    spring:
      datasource:
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://127.0.0.1:3306/db_test?useUnicode=true&characterEncoding=utf8
        password: abc123
    mybatis:
      mapper-locations: classpath:mapper/*.xml
    

    这里我用的MySQL8.0 大家注意一下 MySQL8.0的数据库驱动的类的包改名了

    在前面我有讲过SpringBoot中如何整合Mybatis,在这里我就不累述,有需要的话看这篇文章

    user.sql

    CREATE TABLE `user` (
      `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
      `username` varchar(32) NOT NULL COMMENT '用户名',
      `svc_num` varchar(32) DEFAULT NULL COMMENT '用户号码',
      `password` varchar(100) DEFAULT NULL COMMENT '密码',
      `cust_id` bigint(20) DEFAULT NULL COMMENT '客户id  1对1',
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
    

    对应的UserMapper.java

    package com.yukong.mapper;
    
    import com.yukong.entity.User;
    
    /**
     *
     * @author yukong
     * @date 2019-04-11 16:50
     */
    public interface UserMapper {
    
        int insertSelective(User record);
    
        User selectByUsername(String  username);
    
    
    }
    

    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.yukong.mapper.UserMapper">
      <resultMap id="BaseResultMap" type="com.yukong.entity.User">
        <id column="id" jdbcType="BIGINT" property="id" />
        <result column="username" jdbcType="VARCHAR" property="username" />
        <result column="svc_num" jdbcType="VARCHAR" property="svcNum" />
        <result column="password" jdbcType="VARCHAR" property="password" />
        <result column="cust_id" jdbcType="BIGINT" property="custId" />
      </resultMap>
      <sql id="Base_Column_List">
        id, username, svc_num, `password`, cust_id
      </sql>
      <select id="selectByUsername" parameterType="java.lang.String" resultMap="BaseResultMap">
        select
        <include refid="Base_Column_List" />
        from user
        where username = #{username,jdbcType=VARCHAR}
      </select>
    
      <insert id="insertSelective" keyColumn="id" keyProperty="id" parameterType="com.yukong.entity.User" useGeneratedKeys="true">
        insert into user
        <trim prefix="(" suffix=")" suffixOverrides=",">
          <if test="username != null">
            username,
          </if>
          <if test="svcNum != null">
            svc_num,
          </if>
          <if test="password != null">
            `password`,
          </if>
          <if test="custId != null">
            cust_id,
          </if>
        </trim>
        <trim prefix="values (" suffix=")" suffixOverrides=",">
          <if test="username != null">
            #{username,jdbcType=VARCHAR},
          </if>
          <if test="svcNum != null">
            #{svcNum,jdbcType=VARCHAR},
          </if>
          <if test="password != null">
            #{password,jdbcType=VARCHAR},
          </if>
          <if test="custId != null">
            #{custId,jdbcType=BIGINT},
          </if>
        </trim>
      </insert>
    </mapper>
    

    在这里我们定义了两个方法。

    国际惯例ctrl+shift+t创建mapper的测试方法,并且插入一条记录

    package com.yukong.mapper;
    
    import com.yukong.SpringbootSpringsecurityApplicationTests;
    import com.yukong.entity.User;
    import org.junit.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.crypto.password.PasswordEncoder;
    
    import static org.junit.Assert.*;
    
    /**
     * @author yukong
     * @date 2019-04-11 16:53
     */
    
    public class UserMapperTest extends SpringbootSpringsecurityApplicationTests {
    
    
        @Autowired
        private UserMapper userMapper;
    
    
        @Test
        public void insert() {
            User user = new User();
            user.setUsername("yukong");
            user.setPassword("abc123");
            userMapper.insertSelective(user);
        }
    
    }
    

    运行测试方法,并且成功插入一条记录。

    创建UserController.java

    package com.yukong.controller;
    
    import com.yukong.entity.User;
    import com.yukong.mapper.UserMapper;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    /**
     * @author yukong
     * @date 2019-04-11 15:22
     */
    @RestController
    public class UserController {
        
        @Autowired
        private UserMapper userMapper;
    
        @RequestMapping("/user/{username}")
        public User hello(@PathVariable String username) {
            return userMapper.selectByUsername(username);
        }
    
    }
    
    

    这个方法就是根据用户名去数据库查找用户详细信息。

    启动。因为我们之前插入过一条username=yukong的记录,所以我们查询一下,访问127.0.0.1:8080/user/yukong

    [图片上传失败...(image-ea02ac-1554981869345)]

    我们可以看到 我们被重定向到了一个登录界面,这也是我们之前引入的spring-boot-security-starter起作用了。

    大家可能想问了,用户名跟密码是什么,用户名默认是user,密码在启动的时候已经通过日志打印在控制台了。

    image-20190411171141031.png

    现在我们输入用户跟密码并且登录。就可以成功访问我们想要访问的接口。

    image-20190411171540312

    从这里我们可以知道,我只需要引入了Spring-Security的依赖,它就开始生效,并且保护我们的接口了,但是现在有一个问题就是,它的用户名只能是user并且密码是通过日志打印在控制台,但是我们希望它能通过数据来访问我们的用户并且判断登录。

    其实想实现这个功能也很简单。这里我们需要了解两个接口。

    • UserDetails
    • UserDetailsService
    UserDetails

    所以,我们需要将我们的User.java实现这个接口

    package com.yukong.entity;
    
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.authority.AuthorityUtils;
    import org.springframework.security.core.userdetails.UserDetails;
    
    import java.util.Collection;
    
    /**
     *
     * @author yukong
     * @date 2019-04-11 16:50
     */
    public class User implements UserDetails {
        /**
        * 主键
        */
        private Long id;
    
        /**
        * 用户名
        */
        private String username;
    
        /**
        * 用户号码
        */
        private String svcNum;
    
        /**
        * 密码
        */
        private String password;
    
        /**
        * 客户id  1对1
        */
        private Long custId;
    
        public Long getId() {
            return id;
        }
    
        public void setId(Long id) {
            this.id = id;
        }
    
        @Override
        public String getUsername() {
            return username;
        }
    
        @Override
        public boolean isAccountNonExpired() {
            return false;
        }
    
        @Override
        public boolean isAccountNonLocked() {
            return false;
        }
    
        @Override
        public boolean isCredentialsNonExpired() {
            return false;
        }
    
        @Override
        public boolean isEnabled() {
            return false;
        }
    
        public void setUsername(String username) {
            this.username = username;
        }
    
        public String getSvcNum() {
            return svcNum;
        }
    
        public void setSvcNum(String svcNum) {
            this.svcNum = svcNum;
        }
    
        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
            // 这里我们没有用到权限,所以返回一个默认的admin权限
            return AuthorityUtils.commaSeparatedStringToAuthorityList("admin");
        }
    
        @Override
        public String getPassword() {
            return password;
        }
    
        public void setPassword(String password) {
            this.password = password;
        }
    
        public Long getCustId() {
            return custId;
        }
    
        public void setCustId(Long custId) {
            this.custId = custId;
        }
    }
    

    接下来我们再看看UserDetailsService

    image

    它只有一个方法的声明,就是通过用户名去查找用户信息,从这里我们应该知道了,SpringSecurity回调UserDetails#loadUserByUsername去获取用户,但是它不知道用户信息存在哪里,所以定义成接口,让使用者去实现。在我们这个项目用 我们的用户是存在了数据库中,所以我们需要调用UserMapper的方法去访问数据库查询用户信息。这里我们新建一个类叫MyUserDetailsServiceImpl

    package com.yukong.config;
    
    import com.yukong.mapper.UserMapper;
    import org.springframework.beans.factory.annotation.Autowired;
    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;
    
    /**
     * @author yukong
     * @date 2019-04-11 17:35
     */
    @Service
    public class MyUserDetailServiceImpl implements UserDetailsService {
    
        @Autowired
        private UserMapper userMapper;
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            return userMapper.selectByUsername(username);
        }
    }
    
    

    然后新建一个类去把我们的UserDetailsService配置进去

    这里我们新建一个SecurityConfig

    package com.yukong.config;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    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.WebSecurityConfigurerAdapter;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.security.crypto.password.PasswordEncoder;
    
    /**
     * @author yukong
     * @date 2019-04-11 15:08
     */
    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Autowired
        private UserDetailsService userDetailsService;
    
    
        @Bean
        public PasswordEncoder passwordEncoder(){
            return new BCryptPasswordEncoder();
        }
    
        @Autowired
        public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
            // 配置UserDetailsService 跟 PasswordEncoder 加密器
            auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
            auth.eraseCredentials(false);
        }
    }
    
    

    在这里我们还配置了一个PasswordEncoder加密我们的密码,大家都知道密码明文存数据库是很不安全的。

    接下里我们插入一条记录,需要注意的是 密码需要加密。

    package com.yukong.mapper;
    
    import com.yukong.SpringbootSpringsecurityApplicationTests;
    import com.yukong.entity.User;
    import org.junit.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.crypto.password.PasswordEncoder;
    
    import static org.junit.Assert.*;
    
    /**
     * @author yukong
     * @date 2019-04-11 16:53
     */
    
    public class UserMapperTest extends SpringbootSpringsecurityApplicationTests {
    
        @Autowired
        private PasswordEncoder passwordEncoder;
    
        @Autowired
        private UserMapper userMapper;
    
    
        @Test
        public void insert() {
            User user = new User();
            user.setUsername("yukong");
            user.setPassword(passwordEncoder.encode("abc123"));
            userMapper.insertSelective(user);
        }
    
    }
    

    接下来启动程序,并且登录,这次只需要输入插入到数据中的那条记录的用户名跟密码即可。

    在这里一节中,我们了解到如何使用springsecurity 完成一个登录功能,接下我们将通过分析源码来了解为什么需要这个配置,以及SpringSecurity的工作原理是什么。

    相关文章

      网友评论

        本文标题:【SpringSecurity系列01】初识SpringSecu

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