天天看点

“安全”地学习Spring Security放心学,很安全

放心学,很安全

Spring Security可以说是Java Web开发必须要学的技能 了,就像我每天早上起来都要望一望镜子中帅气的我(yue~),好啦,废话不多说,一起来学习一下Spring Security吧,冲冲冲

“安全”地学习Spring Security放心学,很安全

1. 什么是 Spring Security ?

Spring Security是基于spring的轻量级安全框架,与spring mvc有很好的继承,提供了全面的安全性解决方案,同时在web请求级和方法调用级处理身份确认和授权。在spring的基础上,security充分利用了依赖注入和面向切面基础,为应用系统提供了声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。

1.1 功能

  • 认证(你是谁,用户/设备/系统)
  • 验证(你能干嘛,也叫权限控制/授权,允许执行的操作)

1.2 原理

基于Filter,Servlet,AOP实现身份认证和权限验证

1.3 管中窥豹

下面我们就用一个小栗子来先体验一下吧~

  1. 我们先创建一个空的工程和maven模块
  2. 然后加入依赖
<!--加入sb-->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-parent</artifactId>
        <version>2.0.6.RELEASE</version>
    </parent>
    <!--指定依赖-->
    <dependencies>
        <!--web开发相关依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--security-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
           
  1. 创建启动类
@SpringBootApplication
public class FirstApplication {

    public static void main(String[] args) {
        SpringApplication.run(FirstApplication.class,args);
    }
}
           
  1. 创建controller,接收请求
@RestController
@RequestMapping("/hello")
public class HelloSecurityController {

    @RequestMapping("/world")
    public String sayHello(){
        return "spring security 安全框架";
    }
}
           
  1. 启动项目,可以看到启动日志出现一个uuid密码。这是security的默认临时密码,每次都不一样
    “安全”地学习Spring Security放心学,很安全
  2. 访问项目,会出现默认的登录界面,我们输入默认用户名user,然后随便输入密码,可以发现报错;我们把密码换成默认的密码,则登录成功
    “安全”地学习Spring Security放心学,很安全
    “安全”地学习Spring Security放心学,很安全

1.4 自定义

从上面看到security会有默认的用户名和密码,当然我们也可以在application.yml配置文件中进行自定义

spring:
  security:
    user:
      name: root
      password:  root
           

1.5 关闭验证功能

要关闭验证功能,只需将启动类的启动注解改成

@SpringBootApplication(exclude = {SecurityAutoConfiguration.class})

,即可

2. 基于内存用户信息

我们不可能都在配置文件中设置用户名和密码,这样的操作量太多了;这里我们可以继承

WebSecurityConfigurerAdapter

,重写方法

protected void configure(AuthenticationManagerBuilder auth)

,实现自定义认证信息

  1. 创建

    config.MyWebSecurityConfig

    配置类
@Configuration//标识当前是一个配置类,这个类方法的返回值是java对象,这些对象放入到spring容器中
@EnableWebSecurity//开启security
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {//继承WebSecurityConfigurerAdapter

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {//在重写方法中配置用户和密码信息,作为登录的数据
        //下面的密码要调用密码加密类进行加密,不然会报错(security5后)
        PasswordEncoder pe = passwordEncoder();
        //例如在内存中预先保存两个用户登录信息
        auth.inMemoryAuthentication()
                .withUser("aa")
                .password(pe.encode("654321"))
                .roles();
        auth.inMemoryAuthentication()
                .withUser("bb")
                .password(pe.encode("123456"))
                .roles();
        auth.inMemoryAuthentication()
                .withUser("cc")
                .password(pe.encode("123456"))
                .roles();
    }

    @Bean//把方法的返回值对象,放到spring的容器中
    public PasswordEncoder passwordEncoder(){//创建密码加密类
        //创建PasswordEncoder的实现类,实现类是加密算法,有很多种加密算法实现类
        return new BCryptPasswordEncoder();
    }
}
           

这样所设置的用户就可以用对应的密码进行登录啦

2.1 角色设置

基于角色role的身份认证同一个用户可以有不同的角色,同时可以开启对方法级别的认证

  1. 配置
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)//启动方法界别的认证,默认是false,true表可以使用@PreAuthorize和@ProAuthorize
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        PasswordEncoder pe = passwordEncoder();
        //定义两个角色 normal,admin
        auth.inMemoryAuthentication()
                .withUser("aa")
                .password(pe.encode("654321"))
                .roles("normal");
        auth.inMemoryAuthentication()
                .withUser("bb")
                .password(pe.encode("123456"))
                .roles("normal");
        auth.inMemoryAuthentication()
                .withUser("cc")
                .password(pe.encode("123456"))
                .roles("normal","admin");//注意角色不同
    }

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
}
           
  1. 方法设置

    在对应方法上添加注解

    @PreAuthorize

@RequestMapping("/helloUser")
    @PreAuthorize(value = "hasAnyRole('admin','normal')")//注意要单引号
    public String helloCommonUser(){//normal和admin都可访问
        return "hello 拥有normal,admin角色的用户";
    }

    @RequestMapping("/helloAdmin")
    @PreAuthorize(value = "hasAnyRole('admin')")
    public String helloAdmin(){//指定admin访问
        return "hello 拥有admin角色的用户";
    }
           
  1. 启动,访问

    我们用普通用户bb访问admin专属资源

    “安全”地学习Spring Security放心学,很安全
    可以看到无法访问,接着我们用有双重身份的cc区访问admin资源,则会发现可以访问

2.1.1 设置用户角色访问步骤

  1. 继承

    WebSecurityConfigurerAdapter

    ,重写

    configure()

    方法,指定用户roles
  2. 在类上加入启动方法级别的注解
  3. 在处理器方法上加入角色信息,指定方法可以访问的角色列表

3. 基于JDBC的用户认证

对于小型,自己玩的项目可以用上面的基于内存的权限管理,但是大项目,大用户量用这种就是脑子瓦特了,所以接下来我们要介绍基于jdbc的用户认证,从数据库mysql中获取用户身份信息(用户名,密码和角色)

security 对象用户信息的表示类是

UserDetails

UserDetails

是一个接口,高度抽象的用户信息类(相当于项目中的User类);需要向security提供

User

对象,这个对象的数据来自数据库查询

我们可以在

UserDetails

中 按

ctrl+h

查看当前的实现类,

User

类 是

UserDetails

接口的实现类,构造方法有三个参数,

username

password

authorities

3.1 实现步骤

  1. 创建项目

    创建新module,选择webapp模板,在main目录下创建java和resources目录和指定。依赖直接copy前面的依赖情况,再加上jdbc和jpa的依赖

    “安全”地学习Spring Security放心学,很安全
<!--    mysql驱动-->
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>5.1.9</version>
    </dependency>
<!--    数据库访问框架-->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
           
  1. 创建dao和service
    “安全”地学习Spring Security放心学,很安全
    Enrity
//标识当前类是实体类,表示数据库中的一个表,表名默认和类名一致
@Entity
public class UserInfo {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String username;

    private String password;
    //角色
    private String role;

    //省略get/set方法
}
           

Dao

public interface UserInfoDao extends JpaRepository<UserInfo,Long> {
    //按username查询数据库信息
    UserInfo findByUsername(String username);
}
           

Service

public interface UserInfoService {
    UserInfo findUserInfo(String username);
}
           
@Service
public class UserInfoServiceImpl implements UserInfoService {
    @Autowired
    private UserInfoDao dao;

    @Override
    public UserInfo findUserInfo(String username) {
        UserInfo userInfo = dao.findByUsername(username);
        return userInfo;
    }
}
           
  1. 创建配置文件和启动类

    application.properties

spring.datasource.url=jdbc:mysql://localhost:3306/sb-jpa?serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.initialization-mode=always
#springboot2.x以上需要加这一句配置,不然无法执行sql和建表

spring.jpa.generate-ddl=true
spring.jpa.show-sql=true
spring.jpa.database=mysql
           

启动类

启动,可以看到数据库创建了相应的表

4. 初始化用户信息

现在表格还是空的,我们创建一个初始化类,在当中添加一些用户信息

创建init.JdbcInit

@Component
public class JdbcInit {
    @Autowired
    private UserInfoDao dao;

    @PostConstruct
    public void init(){
        PasswordEncoder pe = new BCryptPasswordEncoder();
        UserInfo u = new UserInfo();
        u.setUsername("tiao");
        u.setPassword(pe.encode("123456"));
        u.setRole("normal");
        dao.save(u);

        u = new UserInfo();
        u.setUsername("admin");
        u.setPassword(pe.encode("admin"));
        u.setRole("admin");
        dao.save(u);
    }
}
           

启动后可以看到表格新增了用户信息,密码是加密后的内容

“安全”地学习Spring Security放心学,很安全
  1. 实现

    UserDetailsService

    接口,重写

    UserDetails loadUserByUsername(String vat1)

    ,在方法中获取数据库的用户信息,即执行数据库查询,条件是用户名称

    provider.MyUserDetailService

@Component("MyUserDetailService")//别名
public class MyUserDetailService implements UserDetailsService {
    @Autowired
    private UserInfoDao dao;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = null;
        UserInfo userInfo = null;
        if(username != null){
            userInfo = dao.findByUsername(username);
            if(userInfo != null){
                List<GrantedAuthority> list = new ArrayList<>();
                //角色必须以ROLE_开头
                GrantedAuthority authority = new SimpleGrantedAuthority("ROLE_"+userInfo.getRole());
                list.add(authority);
                //创建User对象
                user = new User(userInfo.getUsername(),userInfo.getPassword(),list);
            }

        }
        return user;
    }
}

           
  1. 创建配置类(copy前面的就好~)
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    @Qualifier("MyUserDetailService")//找到对应别名
    private UserDetailsService userDetailsService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //指定加密类型
        auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
    }
}
           
  1. 创建控制器的方法(还是copy~~)

  2. 启动项目

    记得把init的

    @Component

    @PostConstruct

    注释掉,不然启动项目继续添加用户信息
  3. 效果展示

3.2 RBAC

RBAC是基于角色的访问控制(Role-Based Access Control);在RBAC中,权限和角色相关联,用户通过称为适当的角色成员而得到这些角色的权限。这极大地简化了权限的管理。这样管理都是层级相互依赖的,权限赋予给角色,而角色又赋予用户,这样的权限设计清楚,管理方便

用户是属于角色的,角色拥有权限的集合。用户属于某个角色,他就有角色对应的权限
“安全”地学习Spring Security放心学,很安全

3.2.1 RBAC表的设计

一般RBAC中表的设计可以分为以下

  • 用户表:用户认证(登录用到的表);用户名,密码,是否启动,是否锁定
  • 角色表:定义角色信息;角色名称,角色描述
  • 用户和角色关系表:用户和角色是多对多的关系,一个用户可以有多个角色,一个角色可以有多个用户
  • 权限表:角色有哪些权限
  • 角色和权限关系表

3.3 详解认证中的接口和类

3.3.1 认证类

UserDetailsService

我们上面的一系列操作都离不开这个

UserDetailsService

认证类,下面我们就来详细看看是什么原理吧

  • UserDetails

    :接口,表示用户信息的
    “安全”地学习Spring Security放心学,很安全

    User

    就实现了

    UserDetails

    “安全”地学习Spring Security放心学,很安全
    可以自定义类实现

    UserDetails

    接口,作为自己系统的用户类,这个类可以交给security使用
  • UserDetailsService

    :我们需要实现

    loadUserByUsername()

    方法来根据用户名称,来获取用户信息(用户名称,密码,角色集合,是否可用,是否锁定等信息)
public interface UserDetailsService {
    UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}
           
    • InMemoryUserDetailsManager

      :在内存中维护用户信息

      使用方便,但数据不持久,系统重启后数据恢复原样

    • JdbcUserDetailsManager

      :用户信息存放在数据库中,底层使用

      JdbcDaoImpl

      操作数据库;可以用

      JdbcUserDetailsManager

      中的方法完成用户的管理
      “安全”地学习Spring Security放心学,很安全
      其中有很多方法如

      createUser

      updateUser

      deleteUser

      userExeists

我们可以打开

org.springframework.security.core.userdetails.jdbc

,其中有个users.ddl定义文件

“安全”地学习Spring Security放心学,很安全
“安全”地学习Spring Security放心学,很安全

3.4 实现

  1. InMemoryUserDetailsManager

  • 我们先创建一个快速maven模块
  • 加入依赖(sb、security和web)
  • 创建应用的配置类
    • 创建密码的处理类对象

      config.ApplicationConfig

    • 使用

      InMemoryUserDetailsManager

      创建用户
@Configuration
public class ApplicationConfig {
    //创建PasswordEncoder
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
    @Bean
    public UserDetailsService userDetailsService(){//创建UserDetailsService的实现类对象
        PasswordEncoder encoder = passwordEncoder();
        //创建内存的UserDetailsService的实现类对象
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        //创建用户
        manager.createUser(User
                .withUsername("admin")
                .password(encoder.encode("admin"))
                .roles("ADMIN","USER")
                .build());
        manager.createUser(User
                .withUsername("tiao")
                .password(encoder.encode("123"))
                .roles("USER")
                .build());
        //。。。。
        return manager;
    }
}
           
  • 创建类继承

    WebSecurityConfigurerAdapter

    ,自定义安全配置
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserDetailsService detailsService = null;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.userDetailsService(detailsService);
    }
}
           
  • 创建控制器方法和启动类
  • 测试
    “安全”地学习Spring Security放心学,很安全
  1. JdbcUserDetailsManager

  • 创建表

    我们前面打开了ddl文件,我们将其拷贝到SQLyog中,将其中的 varchar_ignorecase 类型改成 mysql 的 varchar类型,然后执行,可以看到新建了两个表

    “安全”地学习Spring Security放心学,很安全
  • 新建module:新建一个quickmodule,略
  • 加入依赖:在上一部分的基础上,加上mysql和jdbc的依赖
  • 创建应用的配置类,创建

    JdbcUserDetailsService

    对象:获取数据库中users表的数据
@Configuration
public class ApplicationConfig {
    @Autowired
    private DataSource dataSource;
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    //创建JdbcUserDetailsService对象
    @Bean(name = "jdbcUserDetailsService")
    public UserDetailsService jdbcUserDetailsService(){
        PasswordEncoder encoder = passwordEncoder();
        //初始数据源DataSource,JdbcUserDetailsManager的构造方法是带datasource参数的
        JdbcUserDetailsManager manager = new JdbcUserDetailsManager(dataSource);
        manager.createUser(User.withUsername("admin")
                .password(encoder.encode("admin"))
                .roles("ADMIN","USER","MANAGER").build());
        manager.createUser(User.withUsername("aa")
                .password(encoder.encode("aa"))
                .roles("USER").build());
        manager.createUser(User.withUsername("bb")
                .password(encoder.encode("bb"))
                .roles("USER","NORMAL").build());
        return manager;
    }
}
           
  • 创建一个security的配置,自定义安全配置信息,指定

    JdbcUserDetailsService

@EnableWebSecurity
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    @Qualifier("jdbcUserDetailsService")
    private UserDetailsService userDetailsService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        super.configure(http);
        http.userDetailsService(userDetailsService);
    }
}
           
  • 修改application.properties文件:连接数据库,配置数据源DataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/sb-jpa
spring.datasource.username=root
spring.datasource.password=root
           
  • 测试,略

3.4.1 实现表单登录

前面都是用security自带创建的表格,接下来我们自行创建表格来测试

  • 创建用户表
    “安全”地学习Spring Security放心学,很安全
  • 创建角色表
    “安全”地学习Spring Security放心学,很安全
  • 创建用户角色关系表
    “安全”地学习Spring Security放心学,很安全
  • 构建快速maben项目
  • 加入依赖:比前面的依赖多一个mybatis
<dependency>
      <groupId>org.mybatis.spring.boot</groupId>
      <artifactId>mybatis-spring-boot-starter</artifactId>
      <version>2.0.1</version>
</dependency>
           
  • 编写application.properties:连接数据库,创建连接池
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/sb-jpa
spring.datasource.username=root
spring.datasource.password=root

mybatis.config-location=classpath:/mapper/*Mapper.xml
mybatis.type-aliases-package=com.tiao.entity
           
  • 创建自己的user类,代替UserDetails
public class SysUser implements UserDetails {

    private int id;
    private String username;
    private String password;
    private String realname;
    private boolean isExpired;
    private boolean isLocked;
    private boolean idCredentials;
    private boolean isEnabled;
    private Date createTime;
    private Date loginTime;
    private List<GrantedAuthority> authorities;

    public SysUser() {
    }

    public SysUser(int id,String username, String password, String realname, boolean isExpired,
                   boolean isLocked, boolean idCredentials, boolean isEnabled, Date createTime,
                   Date loginTime, List<GrantedAuthority> authorities) {
        this.id = id;
        this.username = username;
        this.password = password;
        this.realname = realname;
        this.isExpired = isExpired;
        this.isLocked = isLocked;
        this.idCredentials = idCredentials;
        this.isEnabled = isEnabled;
        this.createTime = createTime;
        this.loginTime = loginTime;
        this.authorities = authorities;
    }

    public int getId() {
        return id;
    }

    public boolean isExpired() {
        return isExpired;
    }

    public Date getCreateTime() {
        return createTime;
    }

    public Date getLoginTime() {
        return loginTime;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return username;
    }

    @Override
    public boolean isAccountNonExpired() {
        return isExpired;
    }

    @Override
    public boolean isAccountNonLocked() {
        return isLocked;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return idCredentials;
    }

    @Override
    public boolean isEnabled() {
        return isEnabled;
    }

    public String getRealname() {
        return realname;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public void setRealname(String realname) {
        this.realname = realname;
    }

    public void setExpired(boolean expired) {
        isExpired = expired;
    }

    public void setLocked(boolean locked) {
        isLocked = locked;
    }

    public void setIdCredentials(boolean idCredentials) {
        this.idCredentials = idCredentials;
    }

    public void setEnabled(boolean enabled) {
        isEnabled = enabled;
    }

    public void setCreateTime(Date createTime) {
        this.createTime = createTime;
    }

    public void setLoginTime(Date loginTime) {
        this.loginTime = loginTime;
    }

    public void setAuthorities(List<GrantedAuthority> authorities) {
        this.authorities = authorities;
    }
}

//省略get/set方法
           

mapper.SysUserMapper

//@Repository:创建dao对象
@Repository
public interface SysUserMapper {
    int insertSysUser(SysUser user);
}
           

mapper. SysUserMapper

//@Repository:创建dao对象
@Repository
public interface SysUserMapper {
    int insertSysUser(SysUser user);

    //根据账号名称,获取用户信息
    SysUser selectSysUser(String username);
}
           

创建mapper文件

<mapper namespace="com.tiao.mapper.SysUserMapper">
    <insert id="insertSysUser" >
        insert into sys_user(username,password,realname,
            isenable,islock,iscredentials,createtime,logintime)
        values(#{username},#{password},#{realname},#{isEnabled},
            #{isLocked},#{idCredentials},#{createTime},#{loginTime},)
    </insert>
</mapper>
           

启动类

@MapperScan(value = "com.tiao.mapper")
@SpringBootApplication
public class UserRoleApplication {
    public static void main(String[] args) {
        SpringApplication.run(UserRoleApplication.class,args);
    }
}
           

启动类

@MapperScan(value = "com.tiao.mapper")
//@ComponentScan(basePackages = "com.tiao.mapper")
@SpringBootApplication
public class UserRoleApplication {
    @Autowired
    SysUserMapper userMapper;
    public static void main(String[] args) {
        SpringApplication.run(UserRoleApplication.class,args);
    }

    @PostConstruct
    public void jdbcInit(){
        Date curDate = new Date();
        PasswordEncoder encoder = new BCryptPasswordEncoder();

        List<GrantedAuthority> l1 = new ArrayList<>();
        //参数角色名称,需要以“ROLE_”开头,后面加上自定义角色名称,一般是从数据库获得,这里作为演示才这样写
        GrantedAuthority authority = new SimpleGrantedAuthority("ROLE_"+"USER");
        l1.add(authority);
        List<GrantedAuthority> l2 = new ArrayList<>();
        GrantedAuthority authority2 = new SimpleGrantedAuthority("ROLE_"+"READ");
        l2.add(authority2);
        List<GrantedAuthority> l3 = new ArrayList<>();
        GrantedAuthority authority3 = new SimpleGrantedAuthority("ROLE_"+"ADMIN");
        l3.add(authority3);

        SysUser u1 = new SysUser(
                "u1",encoder.encode("123456"),"用户",true,true,true,true,curDate,curDate,l1
        );
        SysUser u2 = new SysUser(
                "u2",encoder.encode("123456"),"读",true,true,true,true,curDate,curDate,l2
        );
        SysUser u3 = new SysUser(
                "u3",encoder.encode("123456"),"管理员",true,true,true,true,curDate,curDate,l3
        );

        userMapper.insertSysUser(u1);
        userMapper.insertSysUser(u2);
        userMapper.insertSysUser(u3);
    }
}
           

初始化数据库信息

“安全”地学习Spring Security放心学,很安全
“安全”地学习Spring Security放心学,很安全
“安全”地学习Spring Security放心学,很安全

创建SysUser查询方法

<!--    定义列和属性的对应关系-->
    <resultMap id="userMapper" type="com.tiao.entity.SysUser">
        <id column="id" property="id"/>
        <result column="username" property="username"/>
        <result column="password" property="password"/>
        <result column="realname" property="realname"/>
        <result column="isenable" property="isEnabled"/>
        <result column="islock" property="isLocked"/>
        <result column="iscredentials" property="isCredentials"/>
        <result column="createtime" property="createTime"/>
        <result column="logintime" property="loginTime"/>
        <result column="isexpire" property="isExpired"/>
    </resultMap>
    <select id="selectSysUser" resultMap="userMapper">
        select id,username,password,realname,isexpire,
        isenable,islock,iscredentials,createtime,logintime
        from sys_user where username=#{username}
    </select>
           

创建SysRoleMapper和mapper文件,和查询角色信息方法

public class SysRole {
    private Integer id;
    private String name;
    private String memo;
//省略get/set
}
           
@Repository
public interface SysRoleMapper {
    List<SysRole> selectRoleByUser(Integer userId);
}
           
<mapper namespace="com.tiao.mapper.SysRoleMapper">
<!--    定义列和属性的对应关系-->
    <resultMap id="roleMapper" type="com.tiao.entity.SysRole">
        <id column="id" property="id"/>
        <result column="rolename" property="rolename"/>
        <result column="rolememo" property="memo"/>
    </resultMap>
    <select id="selectRoleByUser" resultMap="roleMapper">
        SELECT r.id,r.rolename,r.rolememo FROM sys_user_role ur,sys_role r
        WHERE ur.roleid = r.id AND ur.userid=#{userid}
    </select>
</mapper>
           
  • 创建自定义的UserDetailsService实现类,在重写方法中,查询数据库获取用户信息,获取角色数据,构建UserDetails实现类对象
@Service
public class JdbcUserDetailsService implements UserDetailsService {
    @Autowired
    private SysUserMapper userMapper;
    @Autowired
    private SysRoleMapper roleMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //1.根据username获取SysUser
        SysUser user = userMapper.selectSysUser(username);
        if(user != null){
            //2.根据userid获取role
            List<SysRole> roleList = roleMapper.selectRoleByUser(user.getId());
            List<GrantedAuthority> authorities = new ArrayList<>();
            String roleName = "";
            for(SysRole role : roleList){
                roleName = role.getName();
                GrantedAuthority authority = new SimpleGrantedAuthority("ROLE_"+roleName);
                authorities.add(authority);
            }
            user.setAuthorities(authorities);
            return user;

        }
        return user;
    }
}
           
  • 创建继承 WebSecurityConfigrerAdapter:自定义安全配置
@Configuration
@EnableWebSecurity
public class CustomSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    //@Qualifier(value = "jdbcUserDetailsService")
    private UserDetailsService userDetailsService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //  super.configure(auth);
        auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        super.configure(http);
        http.authorizeRequests().antMatchers("/index").permitAll()
                .antMatchers("/access/user/**").hasRole("USER")
                .antMatchers("/access/read/**").hasRole("READ")
                .antMatchers("/access/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
                .and()
                .formLogin();
    }
}
           

-自定义登录

    • 传统form登录

      创建/resources/static/index.html

      “安全”地学习Spring Security放心学,很安全
<p>验证访问</p>
    <a href="/access/user">u1</a>
    <a href="/access/read">u2</a>
    <a href="/access/admin">u3</a>
           
  • 创建controller
@RestController
public class MyController {
    @GetMapping(value = "/access/user",produces = "text/html;charset=utf-8")
    public String sayUser(){
        return "u1 是 user 角色";
    }

    @GetMapping(value = "/access/read",produces = "text/html;charset=utf-8")
    public String sayRead(){
        return "u2 是 read 角色";
    }

    @GetMapping(value = "/access/admin",produces = "text/html;charset=utf-8")
    public String sayAdmin(){
        return "u3 是 admin 角色";
    }
}
           
@Controller
public class IndexController {
    @GetMapping("/index")
    public String toIndexHtml(){
        return "forward:/index.html";
    }
}
           

3.4.2 总结:基于表单的实现步骤

  1. 创建maven项目
  2. 加入依赖
  3. 创建实体类:

    SysUser

    实现

    UserDetails

    SysRole

  4. 配置文件application.properties:配置数据源和mapper相关
  5. mapper类:定义对应方法
  6. mapper配置文件:定义sql
  7. 创建

    UserDetailsService

    接口的实现类:根据username从数据库查询账号信息

    SysUser

    对象,再根据user的id查询SysRole表中的Role信息
  8. 继承

    WebSecurityConfigurerAdapter

    ,对权限进行配置
  9. 创建主类:包括对security数据库信息的初始化
  10. 创建

    /resources/static/index.html

    ,首页文件
  11. 创建controller,包括重定向到首页和接收请求
自定义登录页面:security自带的登录页面是没有美化的,同时默认的登录页面的访问地址是

/login

,请求参数用户名是

username

,密码是

password

(可以在默认的登录页面查看页面元素进行查看);如果我们需要自定义页面的化,需要进行设置;security登录实现的基础是过滤器

AbstractAuthenticationProcessingFilter

自定义过程:

  1. 写自定义登录页面
  2. 在配置文件中写入:自定义登录视图页面

    .loginPage("/a.html");

    ,并且要指定其可以直接访问

    .antMatchers("a.html").permitAll()

  3. 设置form中登录的访问uri地址

    .loginProcessingUrl("/login");

  4. 设置关于跨域访问的安全设置,先禁用

    .csrf().disable();

  5. 如果需要,我们可以再加入404的页面

    .failureUrl(/error.html)

  6. 可以通过 http 对参数进行自定义:

    .usernameParameter("a").passwordParameter("b")

3.4.3 实现ajax登录

基于表单方式很少用,现在都是前后端分离,一般都是使用json作为数据交换格式

  1. 在前面项目的基础上改造,创建新的登录页面

    static/Myajax.html

<script type="text/javascript" src="../static/js/jquery-3.1.0.js"></script>
    <script type="text/javascript">
        $(function () {
            //jquery的入口函数
            $("#btnLogin").click(function () {
                alert("click");
                var uname = $("#username").val();
                var pwd = $("#password").val();
                $.ajax({
                    url: "/login",
                    type: "POST",
                    data: {
                        "username":uname,
                        "password":pwd
                    },
                    dataType:"json",
                    success:function (resp) {
                        alert(resp.msg)
                    }
                })
            })
        })

    </script>
</head>
<body>
    <p>前后端分离的ajax请求方式</p>
    <div>
        用户名:<input type="text" id="username" value=""><br/>
        密码:<input type="text" id="password" value=""><br/>
        <button id="btnLogin">使用ajax登录</button>
    </div>
</body>
           
  1. 设置配置文件

    api说明

  • AuthenticationSuccessHandler

    :security验证用户信息成功后执行的接口,执行其

    onAuthenticationSuccess()

    方法
  • AuthenticationFailureHandler

    :security验证用户失败后执行的接口,执行其

    onAuthenticationFailure()

    方法

    创建

    common.MySuccessHandler

@Component
public class MySuccessHandler implements AuthenticationSuccessHandler {
    /**
     *
     * @param request 请求对象
     * @param response 应答对象
     * @param authentication security验证用户信息成功后的封装类
     */

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request,
                                        HttpServletResponse response,
                                        Authentication authentication) throws IOException, ServletException {
        //登录的用户信息验证成功后执行的方法
        response.setContentType("text/json;charset=utf-8");
        PrintWriter pw = response.getWriter();
        pw.println("{\"msg\":\"登录成功\"}");
        pw.flush();
        pw.close();
    }
}
           

MyFailureHandler

类似,这里省略

设置配置文件

@Configuration
@EnableWebSecurity
public class CustomSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
//    @Qualifier(value = "jdbcUserDetailsService")
    private UserDetailsService userDetailsService;
    //把两个处理类注入进来
    @Autowired
    private AuthenticationFailureHandler failureHandler;
    @Autowired
    private AuthenticationSuccessHandler successHandler;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//        super.configure(auth);
        auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        super.configure(http);
        http.authorizeRequests().antMatchers("/index","/Myajax.html","/login","/js/**").permitAll()
                .antMatchers("/access/user/**").hasRole("USER")
                .antMatchers("/access/read/**").hasRole("READ")
                .antMatchers("/access/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .successHandler(successHandler)
                .failureHandler(failureHandler)
                .loginPage("/Myajax.html")
                .loginProcessingUrl("/login")
                .and()
                .csrf();
    }
}
           
  1. 测试,启动项目,访问index,输入账号密码

    输入错误时

    “安全”地学习Spring Security放心学,很安全
    输入正确时
    “安全”地学习Spring Security放心学,很安全
  2. 使用jackson处理json
  • 加入依赖
<!--    jackson-->
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-core</artifactId>
      <version>2.9.8</version>
    </dependency>
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
      <version>2.9.8</version>
    </dependency>
           
  • 创建消息结果

    vo.Result

public class Result {
    //code=0成功,code=1失败
    private int code;
    //表示错误代码
    private int error;
    //消息文本
    private String msg;
//省略get/set
}
           
  • 修改

    MySuccessHandler

    MyFailureHandler

    同理
response.setContentType("text/json;charset=utf-8");
        Result result = new Result();
        result.setCode(0);
        result.setError(1000);
        result.setMsg("登录成功");
        OutputStream out = response.getOutputStream();
        ObjectMapper om = new ObjectMapper();
        om.writeValue(out,result);
        out.flush();
        out.close();
           
  • 修改ajax的结果
  • 结果测试
    “安全”地学习Spring Security放心学,很安全
    “安全”地学习Spring Security放心学,很安全

3.4.4 实现ajax登录实现总结

  1. 在登录页面中加入jQuery
  2. 在登录页面中加入Ajax
  3. 在登录页面中加入dom对象
  4. 创建handler,实现两个不同的接口,成功和失败
  5. 加入jackson依赖
  6. 创建作为结果的对象Result
  7. 配置handler

3.5 生成验证码

生成验证码可以通过自定义实现,也可以通过开源库来实现;验证码的实现可以基于servlet,也可以使用controller;这里我们通过自定义和controller来实现验证码

  1. 创建

    CaptchaController

@Controller
@RequestMapping("/captcha")
public class CaptchaController {
    //定义值,用于生成验证码图片
    private int width = 120;
    private int height = 30;
    //图片内容在图片的起始点
    private int drawY = 20;
    //文字间隔
    private int space = 15;
    //文字个数
    private  int charCount = 4;
    //验证码内容数组
    private  String chars [] = {"A","B","C","D","E","1","2","3","4","5"};

    //定义方法:生成验证码内容,在图片上写文字
    @GetMapping("/code")
    public void makeCaptchaCode(HttpServletRequest request,HttpServletResponse response){
        /**
         * 验证码:需要在内存中绘制一个图片BufferedImage
         * 向这个图片写入文字,把绘制好的内容的图片响应给请求
         */
        //创建一个透明背景,rgb表颜色
        BufferedImage image = new BufferedImage(width,height,BufferedImage.TYPE_INT_RGB);
        //获取画笔
        Graphics g = image.getGraphics();
        //设置画笔是白色的
        g.setColor(Color.white);
        //把画板涂白
        //fillRect(矩形起始x,矩形起始y,矩形宽,矩形高)
        g.fillRect(0,0,width,height);

        //创建字体
        Font f = new Font("宋体",Font.BOLD,16);
        g.setFont(f);
        /**
         * 在画布上写字
         * 参数:文字,x,xy坐标
         * g.drawString("字",10,drawY)
         */
        StringBuffer buffer = new StringBuffer("");
        int ran = 0;
        int len = chars.length;
        for(int i=0;i<charCount;i++){
            ran = new Random().nextInt(len);
            //获取要写上去的内容
            buffer.append(chars[ran]);
            //设置颜色
            g.setColor(makeColor());
            g.drawString(chars[ran],(i+1)*space,drawY);
        }

        //绘制干扰线
        for(int m=0;m<4;m++){
            g.setColor(makeColor());
            int dot[] = makeLineDot();
            g.drawLine(dot[0],dot[1],dot[2],dot[3]);
        }

        //把生成的验证码存储到session中
        request.getSession().setAttribute("code",buffer.toString());
        //设置没有缓冲
        response.setHeader("Pragma","no-cache");
        response.setHeader("Cache-Control","no-cache");
        response.setDateHeader("Expires",0);
        response.setContentType("image/png");
        OutputStream out = response.getOutputStream();
        //参数:输出的图像,图像格式png,jpg,jpeg
        ImageIO.write(image,"png",out);
        out.flush();
        out.close();

    }

    private Color makeColor(){
        Random random = new Random();
        int r = random.nextInt(255);
        int g = random.nextInt(255);
        int b = random.nextInt(255);

        return new Color(r,g,b);
    }

    private int[] makeLineDot(){
        Random random = new Random();
        int x1 = random.nextInt(width/2);
        int y1 = random.nextInt(height);

        int x2 = random.nextInt(width/2);
        int y2 = random.nextInt(height);

        return new int[]{x1,y1,x2,y2};
    }
}
           
  1. 在配置文件中放行
    “安全”地学习Spring Security放心学,很安全
  2. 修改前端代码
  • 增加验证码部分
<script type="text/javascript" src="../static/js/jquery-3.1.0.js"></script>
    <script type="text/javascript">
        $(function () {
            //jquery的入口函数
            $("#btnLogin").click(function () {
                alert("click");
                var uname = $("#username").val();
                var pwd = $("#password").val();
                //增加验证码参数
                var txtcode = $("#txtcode").val();
                $.ajax({
                    url: "/login",
                    type: "POST",
                    data: {
                        "username":uname,
                        "password":pwd,
                        "code":txtcode
                    },
                    dataType:"json",
                    success:function (resp) {
                        alert("代码:"+resp.code + "提示:"+resp.msg)
                    }
                })
            })
        })
        //增加changeCode函数
        function changeCode() {
            //new Date的目的是让浏览器不使用缓存,每次获取新的内容
            var url="/captcha/code?t="+new Date();
            //使用jQuery的attr函数,设置img标签的src属性
            $("#imagecode").attr("src",url);
        }
    </script>
</head>
<body>
    <p>前后端分离的ajax请求方式</p>
    <div>
        用户名:<input type="text" id="username" value=""><br/>
        密码:<input type="text" id="password" value=""><br/>
        验证码:<input type="text" id="txtcode" value=""><br/>
<!--        图像,显示验证码的值-->
        <img id="imagecode" src="/captcha/code"/>
        <a href="javascript:void(0)" onclick="changeCode">重新获取</a>
        <br/>
        <br/>
        <button id="btnLogin">使用ajax登录</button>
    </div>
</body>
           

前端效果

“安全”地学习Spring Security放心学,很安全
  1. 后端验证码验证
** 过滤器**介绍:整个security都是通过过滤器实现的,用户发起请求,通过多个过滤器才能访问到目标资源;目前用表单登录,验证用户名和密码使用的过滤器就是

UsernamePasswordAuthenticationFilter

思路:我们要在验证username和password之前,先验证验证码的正确与否,即在

UsernamePasswordAuthenticationFilter

之前增加一个自定义过滤器,来判断session中的code和请求中的code是否一致,验证失败则抛出异常,security根据异常觉得身份验证是否正确

自定义过滤器方式:1.直接实现 Filter 接口 2.继承

OncePerRequestFilter

(一个只执行一次的过滤器)
  • 创建过滤器异常处理类

    VerificationException

public class VerificationException extends AuthenticationException {
    public VerificationException(String msg, Throwable t) {
        super(msg, t);
    }

    public VerificationException(String msg) {
        super(msg);
    }

    public VerificationException() {
        super("验证码错误,请重新输入");
    }
}
           
  • 创建过滤器

    VerificationCodeFilter

public class VerificationCodeFilter extends OncePerRequestFilter {

    private MyFailureHandler failureHandler = new MyFailureHandler();
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
        //只有是login操作,才需要这个过滤器参与验证码的使用
        String uri = request.getRequestURI();
        if(!"/login".equals(uri)){
            //过滤器正常执行,不参与验证码操作
            filterChain.doFilter(request,response);
        }else{
            try{
                //登录操作,需要验证code
                verificationCode(request);
                //如果验证通过,过滤器正常执行
                filterChain.doFilter(request,response);
            }catch(VerificationException e){
                Result result = new Result();
                result.setCode(1);
                result.setError(1002);
                result.setMsg("验证码错误!!");
                failureHandler.onAuthenticationFailure(request,response,e);
            }
        }
    }
    private void verificationCode(HttpServletRequest request){
        HttpSession session = request.getSession();
        //获取请求中的code
        String requestCode = request.getParameter("code");
        //获取session中的code
        String sessionCode = "";
        Object attr = session.getAttribute("code");
        if(attr != null){
            sessionCode = (String) attr;
        }

        //处理逻辑
        if(!StringUtils.isEmpty(sessionCode)){
            //在session中的code,用户看到这个code了
            //如果能到这段代码,说明用户已经发起登录请求了
            //session中的现在的这个code就应该无效
            session.removeAttribute("code");
        }

        //判断code是否正确
        if(StringUtils.isEmpty(requestCode) ||
                StringUtils.isEmpty(sessionCode) ||
                !requestCode.equals(sessionCode)){
            //失败
            throw new VerificationException();
        }
    }
}
           
  • 修改异常处理类

    MyFailureHandler

    “安全”地学习Spring Security放心学,很安全
    “安全”地学习Spring Security放心学,很安全
  • 在框架的过滤器链条中增加一个自定义过滤器

*完结撒花~~~~~*

大家可以来关注我的公众号“刘小条”,一起学习,一起感受生活~~

本学习笔记学习自王鹤老师,是本人自己的总结和理解,如有不足,请多指教