Spring Security框架从入门到实战

在看完Shiro框架文章时,我们大致应该知道了,了解了安全框架应该去做哪些事情,并且随着微服务架构的发展,Spring Security框架在安全框架的地位也发展的越来越好,所以,了解Spring Security框架的使用还是很有必要的。这篇博客是用来巩固自己的学习内容,博客来源与:江南一点雨,所发的视频及他发的文章。

Spring Security框架能做什么呢

它和上一篇文章讲的Shiro一样,它用来做授权和认证相关功能

快速了解一下Spring Security的强大之处

  1. 首先创建一个Spring Boot项目,勾上一个 web依赖和一个Spring Security依赖后,创建一个Hello接口
    如下图所示:Spring Security框架从入门到实战_第1张图片
    Spring Security框架从入门到实战_第2张图片
  2. 然后,直接启动项目,在启动项目过程中,我们会看到如下一行日志:
Using generated security password: ed9c6238-9afe-4204-9750-6528bb924e44
  1. 项目启动成功后,像平常一样去访问http://localhost:8080/hello接口,我们会发现,和平时不太一样了,平时都能够直接弹出一个hello的字符串,这次竟然弹出了一个登录界面,再说,我们也压根没有写登录界面呀!
    Spring Security框架从入门到实战_第3张图片
    不用说也知道,肯定是因为有了Spring Security依赖的关系了。那么我们该如何登录呢?
    在登录页面,默认的用户名就是 user,默认的登录密码则是项目启动时控制台打印出来的密码,输入用户名密码之后,就登录成功了,登录成功后,我们就可以访问到 /hello 接口了。
    我们可以一个知道:一个依赖就保护了所以接口,应该比Shiro框架要强大很多吧!

Spring Security结合数据库的完整配置

首先添加依赖

结合了Spring Data jpa,所以导入了下列依赖


<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0modelVersion>
    <parent>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-parentartifactId>
        <version>2.3.2.RELEASEversion>
        <relativePath/> 
    parent>
    <groupId>com.markgroupId>
    <artifactId>securityartifactId>
    <version>0.0.1-SNAPSHOTversion>
    <name>securityname>
    <description>Demo project for Spring Bootdescription>

    <properties>
        <java.version>1.8java.version>
    properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-data-jpaartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-securityartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>
        <dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
            <scope>runtimescope>
        dependency>

        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId>
            <scope>testscope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintagegroupId>
                    <artifactId>junit-vintage-engineartifactId>
                exclusion>
            exclusions>
        dependency>
        <dependency>
            <groupId>org.springframework.securitygroupId>
            <artifactId>spring-security-testartifactId>
            <scope>testscope>
        dependency>
    dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-maven-pluginartifactId>
            plugin>
        plugins>
    build>

project>

完成配置文件的配置

spring.datasource.username=root
spring.datasource.password=root
spring.datasource.url= jdbc:mysql:///test?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai

spring.jpa.database=mysql
spring.jpa.database-platform=mysql
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect

项目的目录结构展示图

Spring Security框架从入门到实战_第4张图片

创建实体类(共两个实体类)

  1. Role(角色类)
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity(name = "t_role")
public class Role {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private String nameZh;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getNameZh() {
        return nameZh;
    }

    public void setNameZh(String nameZh) {
        this.nameZh = nameZh;
    }
}
  1. User(用户类)

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

@Entity(name = "t_user")
public class User implements UserDetails {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String username;
    private String password;
    private boolean accountNonExpired;
    private boolean accountNonLocked;
    private boolean credentialsNonExpired;
    private boolean enabled;
    //这是多对多的一个注解,
    @ManyToMany(fetch = FetchType.EAGER,cascade = CascadeType.PERSIST)
    private List<Role> roles;

    public void setAccountNonExpired(boolean accountNonExpired) {
        this.accountNonExpired = accountNonExpired;
    }

    public void setAccountNonLocked(boolean accountNonLocked) {
        this.accountNonLocked = accountNonLocked;
    }

    public void setCredentialsNonExpired(boolean credentialsNonExpired) {
        this.credentialsNonExpired = credentialsNonExpired;
    }

    public void setEnabled(boolean enabled) {
        this.enabled = enabled;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

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

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

    public List<Role> getRoles() {
        return roles;
    }

    public void setRoles(List<Role> roles) {
        this.roles = roles;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<SimpleGrantedAuthority> authorities = new ArrayList<>();
        for (Role role : getRoles()) {
            authorities.add(new SimpleGrantedAuthority(role.getName()));
        }
        return authorities;
    }

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

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

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

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

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

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

可以看到这两个类当中有Spring data jpa 的相关注解,并且可以看到,我们写的User类它实现了一个UserDetails类,因为Spring Security需要对User进行相关安全控制,所以必要满足Spring Security的相关方法。并且采用Spring Data Jpa 还会生成实体类相对应的表格

Dao层

import com.mark.model.User;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserDao extends JpaRepository<User,Long> {
    User findUserByUsername(String username);
}

Service层

import com.mark.dao.UserDao;
import com.mark.model.User;
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;

@Service
public class UserService implements UserDetailsService {
    @Autowired
    UserDao userDao;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userDao.findUserByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("用户不存在");
        }
        return user;
    }
}

可以看到,这个service ,实现了一个UserDetailsService类,这个也是为了Spring Security的相关配置

在配置Spring Security 配置之前

先启动项目,由于配置了Spring Data Jpa 的关系,它会自动在数据库中生成两个表,为啥是三个表呢?,不是仅仅才两个实体类吗?,除了生成了两个实体类的表还生成了一个,两个实体类的关联表。再使用一个测试类来往数据库的表格插入数据。


import com.mark.dao.UserDao;
import com.mark.model.Role;
import com.mark.model.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import javax.transaction.Transactional;
import java.util.ArrayList;
import java.util.List;

@SpringBootTest
class SecurityApplicationTests {

    @Autowired
    UserDao userDao;
    @Test
    void contextLoads() {
        User u1 = new User();
        u1.setUsername("ncu");
        u1.setPassword("123");
        u1.setAccountNonExpired(true);
        u1.setAccountNonLocked(true);
        u1.setCredentialsNonExpired(true);
        u1.setEnabled(true);
        List<Role> rs1 = new ArrayList<>();
        Role r1 = new Role();
        r1.setName("ROLE_admin");
        r1.setNameZh("管理员");
        rs1.add(r1);
        u1.setRoles(rs1);
        userDao.save(u1);
        User u2 = new User();
        u2.setUsername("MarkZQP");
        u2.setPassword("123");
        u2.setAccountNonExpired(true);
        u2.setAccountNonLocked(true);
        u2.setCredentialsNonExpired(true);
        u2.setEnabled(true);
        List<Role> rs2 = new ArrayList<>();
        Role r2 = new Role();
        r2.setName("ROLE_user");
        r2.setNameZh("普通用户");
        rs2.add(r2);
        u2.setRoles(rs2);
        userDao.save(u2);
    }
}

数据库及运行结果截图
Spring Security框架从入门到实战_第5张图片

下面是最重要的Spring Security的相关配置

import com.fasterxml.jackson.databind.ObjectMapper;
import com.mark.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
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.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

import java.io.PrintWriter;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    UserService userService;
    @Bean
    PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }

    @Bean
    RoleHierarchy roleHierarchy() {
        RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();
        // 权限的相关设计
        // 可以看到,admin权限比user权限大,只有有了admin权限
        hierarchy.setHierarchy("ROLE_admin > ROLE_user");
        return hierarchy;
    }

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

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/js/**", "/css/**", "/images/**");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/admin/**").hasRole("admin") //只有有admin角色才能访问/admin/**接口
                .antMatchers("/user/**").hasRole("user") //只有user角色才能访问/user/**接口
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginProcessingUrl("/doLogin")     // 登录界面提交时需要访问的接口"/doLogin"
                .successHandler((req, resp, authentication) -> {
                // 这是登录成功是所调的函数,返回相关的JSON数据
                // 有了相关的JSON数据,前端就可以进行相关的处理
                    Object principal = authentication.getPrincipal();
                    resp.setContentType("application/json;charset=utf-8");
                    PrintWriter out = resp.getWriter();
                    out.write(new ObjectMapper().writeValueAsString(principal));
                    out.flush();
                    out.close();
                })
                .failureHandler((req, resp, e) -> {
                // 这是登录失败所调的函数,也是返回相关的JSON数据
                // 有了相关的JSON数据,前端就可以进行相关的处理
                    resp.setContentType("application/json;charset=utf-8");
                    PrintWriter out = resp.getWriter();
                    out.write(e.getMessage());
                    out.flush();
                    out.close();
                })
                .permitAll()
                .and()
                .logout()
                .logoutUrl("/logout") // 注销链接所调用的logout接口
                .logoutSuccessHandler((req, resp, authentication) -> {
                    resp.setContentType("application/json;charset=utf-8");
                    PrintWriter out = resp.getWriter();
                    out.write("注销成功");
                    out.flush();
                    out.close();
                })
                .permitAll()
                .and()
                .csrf().disable().exceptionHandling()
                .authenticationEntryPoint((req, resp, authException) -> {
                            resp.setContentType("application/json;charset=utf-8");
                            PrintWriter out = resp.getWriter();
                            out.write("尚未登录,请先登录");
                            out.flush();
                            out.close();
                        }
                );
    }
}

代码这么多,先来了解每一个函数对应的功能吧

  1. 首先,我们创建了一个 SecurityConfig的类,它继承了WebSecurityConfigurerAdapter这个类(这是因为它是Spring Security的相关配置),并且加上了@Configuration这个注解让它成为了一个配置类。并且可以重写WebSecurityConfigurerAdapter的相关方法以完成相关功能。
  2. 首先我们提供了一个 PasswordEncoder 的实例,因为目前的案例还比较简单,因此我暂时先不给密码进行加密,所以返回 NoOpPasswordEncoder 的实例即可。
  3. 第一个configure, 直接就一行代码auth.userDetailsService(userService),它完成的功能就是验证用户的账号密码。
  4. 第二个configure,不拦截"/js/**", "/css/**", "/images/**",这些个信息
  5. 第三个configure,有相关注解。

使用postman进行测试

  1. 直接访问hello接口
    Spring Security框架从入门到实战_第6张图片
  2. 登录 nuc账号,它的角色为admin
    Spring Security框架从入门到实战_第7张图片
    登录成功后,在去访问接口/hello,/admin/hello,/user/hello接口都能访问成功,虽然在权限上设置了user下的接口需要user角色才能访问,但是在权限上给了其它的设置,admin角色可以访问user角色所能访问的。
  3. 登录 mark账号,它的角色为user
    Spring Security框架从入门到实战_第8张图片
    登录成功后,在去访问接口/hello,/user/hello接口都能访问成功,但是,/admin/hello接口,不能访问成功,因为有权限的设置

博客到这里就结束了,可能介绍的不够详细,不过— 江南一点雨— 这个大佬写的特别好,可以去看他的文章。

你可能感兴趣的