【SpringSecurity系列01】初识SpringSecurity

​ 什么是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的工作原理是什么。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容