从零开始理解Spring Security的认证与授权

文章目录

        • 1、权限框架
          • 1.1、概述
        • 2、核心概念
        • 3、SpringSecurity
          • 3.1、简单案例
          • 3.2、自定义登录页
          • 3.3、SpringSecurity基本原理
          • 3.4、认证
            • 3.4.1、基于内存模型实现认证
            • 3.4.2、BCrypt密码加密
            • 3.4.3、基于JDBC数据库实现认证
          • 3.5、授权
        • 4、SpringSecurity整合JWT
          • 4.1、前后端分离的权限方案
          • 4.2、实现登录
          • 4.3、自定义授权管理器

1、权限框架
1.1、概述

​ 权限管理是所有后台系统的都会涉及的一个重要组成部分,主要目的是对不同的人访问资源进行权限的控制,避免因权限控制缺失或操作不当引发的风险问题,如操作错误,隐私数据泄露等问题。那么如何实现权限呢?下面列举以下几个方案:

方案一:使用权限框架 Shiro

​ Shiro是apache旗下一个开源框架,它将软件系统的安全认证相关的功能抽取出来,实现用户身份 认证,权限授权、加密、会话管理等功能,组成了一个通用的安全认证框架。Shiro最大的特点是 不跟任何的框架或者容器捆绑,可以独立运行。

​ 如果项目没有使用到Spring框架,可以考虑使用Shiro。Shiro在小项目使用比较常见。

​ Shiro 最大的问题在于和 Spring 家族的产品进行整合时较为不便。在Spring Boot 推出的很长一段时间里,Shiro 都没有提供相应的 starter,后来虽然有一个 shiro-spring-boot-web-starter 出来,但配置并没有简化多少。所以在 Spring Boot/Spring Cloud 技术栈的微服务项目中,Shiro 几乎不存在优势。

方案二:使用权限框架Spring Security

​ Spring Security是一个功能强大且高度可定制的,主要负责为Java程序提供声明式的身份验证和访问控制的安全框架。其前身是Acegi Security,后来被收纳为Spring的一个子项目,并更名为了Spring Security。

优点:

  • Spring Security基于Spring开发,所以Spring Security与Spring更契合;

  • Spring Security功能比Shiro更加强大,尤其是在安全防护方面;

  • Spring Security社区资源比Shiro更加丰富;

  • Spring Boot/Spring Cloud环境中,更容易集成Spring Security;

  • Spring Security 具备良好的扩展性,可以满足自定义的要求;

  • Spring Security对 OAuth2框架支持很好,而Shiro则对 OAuth2 支持不够。

方案三:使用拦截器(过滤器)+JWT 实现地址鉴权

方案四:使用AOP实现方法鉴权

2、核心概念

认证

​ 请求认证: 判断一个用户是否为合法用户的处理过程,最常用的简单身份认证方式是系统通过核对用户输入的用户名和口令,看其是否与系统中存储的该用户的用户名和口令一致,来判断用户身份是否正确,如下图所示:

概念图解

授权

​ 授权就是用户登录后,控制用户是否有权限访问某些资源。如下图所示:

访问控制图解

3、SpringSecurity

​ Spring Security 是 Spring家族中的一个安全管理框架。相比与另外一个安全框架Shiro,它提供了更丰富的功能,社区资源也比Shiro丰富。一般来说中大型的项目都是使用SpringSecurity来做安全框架。小项目有Shiro的比较多,因为相比与SpringSecurity,Shiro的上手更加的简单。

3.1、简单案例

创建一个空的Springboot项目

​ 这里只写了hello。跑起来,可以在浏览器输入http://localhost:8080/hello 会看到浏览器输出 hello ,目前这个工程,是没有权限控制的。

/**
 * @ClassName
 * @Description
 */
@RestController
public class HelloController {

    @RequestMapping("/hello")
    public String hello() {
        return "hello";
    }

}

运行图

pom.xml 引入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

重新启动项目

​ 浏览器输入 http://localhost:8080/hello 这时会自动跳转到一个默认登陆页面

​ 默认用户名是user,密码会输出在控制台。

在浏览器输入 http://localhost:8080/logout 退出登录。

​ demo就演示完成了,是不是很神奇呀!只是引入了一个依赖,什么代码都没写,就实现了一套简单的权限控制了,当然实际的项目,肯定不能这么做的

3.2、自定义登录页

编写自己的登录页面

创建配置类SecurityConfig,配置登录页

@Configuration
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

        http.formLogin()             //自定义自己编写的登陆页面
                .loginPage("/login.html")    //登录页面设置
                .loginProcessingUrl("/login") //登录访问路径
                .permitAll()//登录页和登录访问路径无需登录也可以访问
                .and()
                .authorizeRequests()
                .antMatchers("/css/**","/images/**").permitAll()
                .anyRequest().authenticated()
                .and()
                .csrf().disable();    //关闭csrf防护
        return http.build();
    }

}

再次运行项目,会看到我们自己写的登录页面,变成下面这个样子啦

修改HelloController 的hello方法

@RequestMapping("/hello")
public String hello(){
    //认证成功,得到认证成功之后用户信息
    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    String userName = authentication.getName();
    return "hello  "+userName;
}

运行后效果如下:

3.3、SpringSecurity基本原理

Spring-Security其内部基础的处理方式就是通过过滤器来实现的,看下刚才的例子用到的一些过滤器,如图所示:

这几个过滤器都是干啥的呢?

  • UsernamePasswordAuthenticationFilter:负责处理我们在登陆页面填写了用户名密码后的登陆请求。入门案例的认证工作主要有它负责。
  • ExceptionTranslationFilter:处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException。
  • FilterSecurityInterceptor:负责权限校验的过滤器。

当然,SpringSecurity过滤器不止这些,只需了解即可!

3.4、认证

​ 在简单案例中,其实已经是一个非常简单的认证,但是用户名是写死的,密码也需要从控制台查看,很显然实际中并不能这么做。下面,来实现基于内存模型的认证以及用户的自定义认证,密码加密等内容。

3.4.1、基于内存模型实现认证

修改配置类 SecurityConfig,添加两个bean的配置

@Bean
PasswordEncoder passwordEncoder() {
    return NoOpPasswordEncoder.getInstance();
}

@Bean
public UserDetailsService users() {
    UserDetails user = User.builder()
        .username("user")
        .password("123456")
        .roles("USER")
        .build();
    UserDetails admin = User.builder()
        .username("admin")
        .password("112233")
        .roles("USER", "ADMIN")
        .build();
    return new InMemoryUserDetailsManager(user, admin);
}

Spring Security 提供了一个 UserDetails的实现类 User,用于用户信息的实例表示。另外,User 提供 Builder 模式的对象构建方式。

  • NoOpPasswordEncoder:这种编码器不进行任何编码,密码以明文形式存储。
  • InMemoryUserDetailsManager:管理存储在内存中的用户。

再次测试,输入用户名 user 密码123456

3.4.2、BCrypt密码加密

​ 明文密码肯定不安全,所以我们需要实现一个更安全的加密方式BCrypt。

​ BCrypt就是一款加密工具,可以比较方便地实现数据的加密工作。也可以简单理解为它内部自己实现了随机加盐处理。例如,使用MD5加密,每次加密后的密文其实都是一样的,这样就方便了MD5通过大数据的方式进行破解。
BCrypt生成的密文长度是60,而MD5的长度是32。

我们现在随便找个类,写个main方法测试一下

public static void main(String[] args) {
   String password = BCrypt.hashpw("000000", BCrypt.gensalt());
   System.out.println(password);
}

输出结果如下:

$2a$10$cRH8iMMh6XO0T.ssZ/8qVOo8ThWs/qfntIH3a7yfpbPd05h9ZGx8y

你运行的结果不是这样的,因为我们这里用了随机盐。

BCrypt提供了一个方法,用于验证密码是否正确。

boolean checkpw = BCrypt.checkpw("000000", "$2a$10$cRH8iMMh6XO0T.ssZ/8qVOo8ThWs/qfntIH3a7yfpbPd05h9ZGx8y");

接下来,我们看代码如何实现

(1)修改配置类SecurityConfig 的passwordEncoder实现类为BCryptPasswordEncoder

@Bean
PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

​ PasswordEncoder的实现类BCryptPasswordEncoder,用于BCrypt密码的解析。

(2)修改配置类SecurityConfig 的users方法中的密码,为加密后的密码

@Bean
public UserDetailsService users() {
    UserDetails user = User.builder()
            .username("user")
            .password("$2a$10$2VCyByZ5oeiXCEN73wvBB.xpmJgPBbZVS/Aallmdyij2G7hmAKQTG")
            .roles("USER")
            .build();
    UserDetails admin = User.builder()
            .username("admin")
            .password("$2a$10$cRH8iMMh6XO0T.ssZ/8qVOo8ThWs/qfntIH3a7yfpbPd05h9ZGx8y")
            .roles("USER", "ADMIN")
            .build();
    return new InMemoryUserDetailsManager(user, admin);
}

(3)再次测试,输入用户名 user 密码123456

3.4.3、基于JDBC数据库实现认证

​ 在Spring Security框架中提供了一个UserDetailsService 接口,它的主要作用是提供用户详细信息。具体来说,当用户尝试进行身份验证时,UserDetailsService 会被调用,以获取与用户相关的详细信息。这些详细信息包括用户的用户名、密码、角色等

执行流程如下:

新创建一个UserDetailsServiceImpl,让它实现UserDetailsService ,代码如下

@Component
public class UserDetailsServiceImpl  implements UserDetailsService {


    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        if(username.equals("user")){
            UserDetails user= User.builder()
                    .username("user")                  .password("$2a$10$8V3UHgnnI/3RCKhg5aklz.sw448DP4.x9P2hFl/fnw99QU86POlgm")
                    .roles("USER")
                    .build();
            return user;
        }
        if(username.equals("admin")){
            UserDetails admin= User.builder()
                    .username("admin")                    .password("$2a$10$pCQMCKRUi7iUXGBd14a3oetcdgD1MwgKLenxXHidq1pcfmuva1QjW")
                    .roles("ADMIN","USER")
                    .build();
            return admin;
        }
        return null;
    }
}
  • 当前对象需要让spring容器管理,所以在类上添加注解@Component
  • 大家注意一下loadUserByUsername方法的返回值,叫做UserDetails,这也是框架给提供了保存用户的类,并且也是一个接口,如果我们有自定义的用户信息存储,可以实现这个接口,我们后边会详细讲解

既然以上能使用这个类来查询用户信息,那么我们之前在SecurityConfig中定义的用户信息,可以注释掉了,如下:

    /*
    @Bean
    public UserDetailsService users() {
        UserDetails user = User.builder()
                .username("user")
                .password("$2a$10$2VCyByZ5oeiXCEN73wvBB.xpmJgPBbZVS/Aallmdyij2G7hmAKQTG")
                .roles("USER")
                .build();
        UserDetails admin = User.builder()
                .username("admin")
                .password("$2a$10$cRH8iMMh6XO0T.ssZ/8qVOo8ThWs/qfntIH3a7yfpbPd05h9ZGx8y")
                .roles("USER", "ADMIN")
                .build();
        return new InMemoryUserDetailsManager(user, admin);
    }*/

我们可以重启项目,然后进行测试,发现跟之前没什么区别,一样是实现了安全校验

当然我们最终不能把用户静态的定义在代码中的,我们需要到数据库去查询用户,我们可以直接使用我们项目中的用户表,实现的步骤如下:

  • 导入相关依赖(数据库、mybaits、lombok等)
  • 添加配置:连接数据库、mybatis配置等(application.yml)
  • 编写实体类和mapper
  • 改造UserDetailsServiceImpl(用户从数据库中获取)

pom 文件添加依赖

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.2.1</version>
</dependency>
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.2.0</version>
</dependency>
<!--MySQL支持-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.19</version>
</dependency>

application.yml添加数据库相关配置

#服务配置
server:
  #端口
  port: 8080
spring:
  application:
    name: springsecurity-demo
  #数据源配置
  datasource:
    druid:
      driver-class-name: com.mysql.jdbc.Driver
      url: jdbc:mysql://192.168.200.146:3306/security_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
      username: root
      password: 123456
# MyBatis配置
mybatis:
  #mapper配置文件
  mapper-locations: classpath*:mapper*/*Mapper.xml
  type-aliases-package: com.demo.project.entity
  configuration:
    # 这个配置会将执行的sql打印出来,在开发或测试的时候可以用
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    # 驼峰下划线转换
    map-underscore-to-camel-case: true
    use-generated-keys: true
    default-statement-timeout: 60
    default-fetch-size: 100

实体类和mapper

用户实体类

@Data
public class User {
    public Long id;

    /**
     * 用户账号
     */
    private String username;

    /**
     * 密码
     */
    private String password;

    /**
     * 真实姓名
     */
    private String nickName;


}

用户mapper,我们只需要定义一个根据用户名查询的方法即可

@Mapper
public interface UserMapper {

    @Select("select * from sys_user where username = #{username}")
    public User findByUsername(String username);
}

改造UserDetailsServiceImpl

@Component
public class UserDetailsServiceImpl implements UserDetailsService {


    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        //查询用户
        User user = userMapper.findByUsername(username);
        if(user == null){
            throw new RuntimeException("用户不存在或已被禁用");
        }
        SimpleGrantedAuthority user_role = new SimpleGrantedAuthority("user");
        SimpleGrantedAuthority admin_role = new SimpleGrantedAuthority("admin");
        List<GrantedAuthority> list = new ArrayList<GrantedAuthority>();

        list.add(user_role);
        list.add(admin_role);

        return new org.springframework.security.core.userdetails.User(user.getUsername()
                ,user.getPassword()
                , list);
    }
}

上述代码中,返回的UserDetails或者是User都是框架提供的类,我们在项目开发的过程中,很多需求都是我们自定义的属性,我们需要扩展该怎么办?

其实,我们可以自定义一个类,来实现UserDetails,在自己定义的类中,就可以扩展自己想要的内容,如下代码:

@Data
public class UserAuth implements UserDetails {

    private String username; //固定不可更改
    private String password;//固定不可更改
    private String nickName;  //扩展属性  昵称
    private List<String> roles; //角色列表


    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        if(roles==null) return null;
        //把角色类型转换并放入对应的集合
        return roles.stream().map(role -> new SimpleGrantedAuthority("ROLE_"+role)).collect(Collectors.toList());
    }

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

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

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

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

然后,我们可以继续改造UserDetailsServiceImpl中检验用户的逻辑,代码如下:

@Component
public class UserDetailServiceImpl implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //查询用户
        User user = userMapper.findByUsername(username);
        if(user == null){
            throw new RuntimeException("用户不存在或已被禁用");
        }
        UserAuth userAuth = new UserAuth();
        userAuth.setUsername(user.getUsername());
        userAuth.setPassword(user.getPassword());
        userAuth.setNickName(user.getNickName());

        //添加角色
        List<String> roles=new ArrayList<>();
        if("user@qq.com".equals(username)){
            roles.add("USER");
            userAuth.setRoles(roles);
        }
        if("admin@qq.com".equals(username)){
            roles.add("USER");
            roles.add("ADMIN");
            userAuth.setRoles(roles);
        }
        return userAuth;
    }
}

修改HelloController,使用getPrincipal()方法读取认证主体对象。

@RestController
public class HelloController {

    @RequestMapping("/hello")
    public String hello(){
        //获取当前登录用户名称
        String name = SecurityContextHolder.getContext().getAuthentication().getName();
        UserAuth userAuth = (UserAuth)SecurityContextHolder.getContext().getAuthentication().getPrincipal();//取出认证主体对象

        return "hello :"+name
上一篇:Java基础知识(五)-Object


下一篇:如何更改手机GPS定位