Spring Security 认证授权详解

1、Spring Security 概述

1.1、Spring Security 简介

Spring Security 是 Spring 家族中的成员。Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性的完整解决方案。

在对与安全方面的两个主要区域是“认证”和“授权”(或者访问控制),一般来说,Web 应用的安全性包括用户认证(Authentication)和用户授权(Authorization)两个部分,这两点也是 Spring Security 重要核心功能。

  • 用户认证指的是:验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。通俗点说就是系统认为用户是否能登录
  • 用户授权指的是验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。通俗点讲就是系统判断用户是否有权限去做某些事情。

1.2、Spring Security 与 Shiro

SpringSecurity 特点:

  • 和 Spring 无缝整合。
  • 全面的权限控制。
  • 专门为 Web 开发而设计。
    • 旧版本不能脱离 Web 环境使用。
    • 新版本对整个框架进行了分层抽取,分成了核心模块和 Web 模块。单独引入核心模块就可以脱离 Web 环境。
  • 重量级。

Shiro 特点:

  • 轻量级。Shiro 主张的理念是把复杂的事情变简单。针对对性能有更高要求的互联网应用有更好表现。
  • 通用性。
    • 好处:不局限于 Web 环境,可以脱离 Web 环境使用。
    • 缺陷:在 Web 环境下一些特定的需求需要手动编写代码定制。

相对于 Shiro,在 SSM 中整合 Spring Security 都是比较麻烦的操作,所以,Spring Security 虽然功能比 Shiro 强大,但是使用反而没有 Shiro 多(Shiro 虽然功能没有Spring Security 多,但是对于大部分项目而言,Shiro 也够用了)。自从有了 Spring Boot 之后,Spring Boot 对于 Spring Security 提供了自动化配置方案,可以使用更少的配置来使用Spring Security。

因此,一般来说,常见的安全管理技术栈的组合是这样的:

  • SSM + Shiro
  • Spring Boot/Spring Cloud + Spring Security

shiro详解:参考该文,单击前往

2、Hello World

使用springboot集成security安全框架,直接快速初始化一个springboot项目,在这里直接将对应的依赖进行添加(也可以在项目的pom当中添加),之后直接将项目构建出来,等待jar包拉取完。

在这里插入图片描述
而后直接写一个controller来进行测试(如下代码所示),之后直接启动项目访问这个controller的路由,会被转到security的登录页面,需要登录之后才能够访问到对应的路由,这里的账号为user,密码在项目启动的日志当中会输出,并且每次启动的时候密码都回发生变化。

@RestController
public class HelloController {

    @GetMapping("/getSecurity")
    public String hello(){
        return "hello security";
    }
}

3、SpringSecurity Web 权限方案

3.1、设置登录校验的账号和密码

方案一:直接在配置文件当中进行配置账号和密码:

spring.security.user.name=yueyue
spring.security.user.password=123456

方案二:使用配置类进行设置:新建配置类继承至WebSecurityConfigurerAdapter 重写 configure 方法

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

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

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        String password = passwordEncoder.encode("123456");
        auth.inMemoryAuthentication().withUser("yueyue").password(password).roles("admin");
    }

方案三:自定义账号密码:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

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

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
       auth.userDetailsService(userDetailsService).passwordEncoder(password());
    }
}
@Service("userDetailsService")
public class UserService implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("role");
        return new User("yueyue",new BCryptPasswordEncoder().encode("123456"),auths);
    }
}

3.2、查数据库进行认证

在这里首先添加相对应的依赖,这里使用mybatis-plus来对数据库进行操作。

        <!-- 连接池依赖 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.20</version>
        </dependency>
        <!-- 数据库依赖 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!-- mybatis-plus依赖 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.0.5</version>
        </dependency>
        <!-- lombok依赖 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

并且在这里我们使用一个表用来记录账号密码,以及对应的实体类、mapper接口、数据库连接配置等等。

# 连接数据库配置
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver 
spring.datasource.url=jdbc:mysql://localhost:3306/springboot?serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=root

# 实体类
@Data
@TableName("user1")
public class User {
    private Long id;
    private String userName;
    private String userPassword;
}

# mapper接口
@Repository
public interface UserMapper extends BaseMapper<User> {
}

之后只需要修改前面进行获取账号密码的方法,代码如下:

    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        QueryWrapper<com.lzq.entity.User> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("user_name",username);
        com.lzq.entity.User user = userMapper.selectOne(queryWrapper);
        if(user == null){
            throw new UsernameNotFoundException("用户名不存在!");
        }
        List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("role");

        return new User(user.getUserName(),new BCryptPasswordEncoder().encode(user.getUserPassword()),auths);
    }

3.3、自定义登录页面

在config当中还需对configure方法进行重写:

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
                // 登录页面设置
                .loginPage("/login.html")
                // 登录访问路径
                .loginProcessingUrl("/user/login")
                // 登录成功后跳转路径
                .defaultSuccessUrl("/getSecurity").permitAll()
                // 不需要验证授权
                .and().authorizeRequests()
                    .antMatchers("/info").permitAll()
                .anyRequest().authenticated()
        ;
        http.csrf().disable();
        // 403没有权限访问页面将跳转到403.html页面
		http.exceptionHandling().accessDeniedPage("/403.html");
    }

这里使用到一个login.html来作为登录的首页,简单的对html加上个form表单:

    <form action="/user/login" method="post">
        用户名:<input type="text" name="username"/>
        <br/>
        密码:<input type="password" name="password"/>
        <br/>
        <input type="submit" value="login">
    </form>

在进行配置之后,每当提交过来的username和password会和之前设置的进行比较,如果相同的话则会跳转到getSecurity表示登录成功,其余皆为405。

并且在这里的表单提交给的参数也必须是username和password,这是因为在进行账号密码校验的时候会通过UsernamePasswordAuthenticationFilter过滤器,而在过滤器当中进行取值的时候最终会获取usernam和password变量。

	public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
	public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";

而这里同样的也可以不用username和password这两个变量,在form表单当中重新指定两个变量用来传递参数,比如说改成account和pass,那么我们只需要在配置类当中加入

3.4、基于角色或权限进行访问控制

  • hasAuthority 方法:如果当前的主体具有指定的权限,则返回 true,否则返回 false
  • hasAnyAuthority 方法:如果当前的主体有任何提供的角色(给定的作为一个逗号分隔的字符串列表)的话,返回true
  • hasRole 方法:如果用户具备给定角色就允许访问,否则出现 403。
  • hasAnyRole方法:同上,多个角色

还是修改前面的配置类来进行校验验证,添加两个路由访问规则,分别给上admin和manager角色才能够对路由进行访问。

        http.formLogin()
                // 登录页面设置
                .loginPage("/login.html")
                // 登录访问路径
                .loginProcessingUrl("/user/login")
                // 登录成功后跳转路径
                .defaultSuccessUrl("/getSecurity").permitAll()
                // 不需要验证授权
                .and().authorizeRequests()
                    .antMatchers("/info").permitAll()
                    // admin角色才能访问
                    .antMatchers("/admin").hasAuthority("admin")
                    // admin或者manager其中一个角色即可访问
                    .antMatchers("/adminManager").hasAnyAuthority("admin","manager")
                    .antMatchers("/role").hasRole("role")
                    .antMatchers("/anyrole").hasAnyRole("role","admin")
                .anyRequest().authenticated()
        ;
        http.csrf().disable();

而对于角色的给定,在前面实现UserDetailsService接口的实现类的loadUserByUsername方法当中进行指定。

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        QueryWrapper<com.lzq.entity.User> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("user_name",username);
        com.lzq.entity.User user = userMapper.selectOne(queryWrapper);
        if(user == null){
            throw new UsernameNotFoundException("用户名不存在!");
        }
        // 指定角色
        List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admin");
		// 而对于hasRole、hasAnyRole 方法的角色需要加上ROLE_前缀
		// List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_role");
        return new User(user.getUserName(),new BCryptPasswordEncoder().encode(user.getUserPassword()),auths);
    }

在这里我们可以直接追到这几个方法的内部查看区别,可以看到对于hasAuthority和hasAnyAuthority是直接使用角色的名称进行操作,而hasRole和hasAnyRole是加上了一个前缀再进行操作。

	private static String hasAuthority(String authority) {
		return "hasAuthority('" + authority + "')";
	}
		private static String hasAnyAuthority(String... authorities) {
		String anyAuthorities = StringUtils.arrayToDelimitedString(authorities, "','");
		return "hasAnyAuthority('" + anyAuthorities + "')";
	}

	private static String hasRole(String rolePrefix, String role) {
		Assert.notNull(role, "role cannot be null");
		Assert.isTrue(rolePrefix.isEmpty() || !role.startsWith(rolePrefix), () -> "role should not start with '"
				+ rolePrefix + "' since it is automatically inserted. Got '" + role + "'");
		return "hasRole('" + rolePrefix + role + "')";
	}
	private static String hasAnyRole(String rolePrefix, String... authorities) {
		String anyAuthorities = StringUtils.arrayToDelimitedString(authorities, "','" + rolePrefix);
		return "hasAnyRole('" + rolePrefix + anyAuthorities + "')";
	}

3.5、注解使用

  • @Secured:判断是否具有角色,另外需要注意的是这里匹配的字符串需要添加前缀“ROLE_“
  • @PreAuthorize:注解适合进入方法前的权限验证, @PreAuthorize 可以将登录用户的 roles/permissions 参数传到方法中。
  • @PostAuthorize:在方法执行后再进行权限验证,适合验证带有返回值的权限。
  • @PreFilter: 进入控制器之前对数据进行过滤
  • @PostFilter :权限验证之后对数据进行过滤
	// 在启动类上加上注解
	@EnableGlobalMethodSecurity(securedEnabled=true)
	// 在接口上加上注解
    @GetMapping("/autoRole")
    @Secured("ROLE_role")
    public String AutoRole(){
        return "auto role";
    }

	// 加在启动类上
	@EnableGlobalMethodSecurity(securedEnabled=true,prePostEnabled = true)
	// 测试接口
	@GetMapping("/autoRole")
    @PreAuthorize("hasAnyRole('ROLE_role')")
    public String AutoRole(){
        return "auto role";
    }
    
    // 加在启动类上
    @EnableGlobalMethodSecurity(securedEnabled=true,prePostEnabled = true)
    // 测试接口
    @GetMapping("/autoRole")
    @PostAuthorize("hasAnyAuthority('admins')")
    public String AutoRole(){
        System.out.println("PostAuthorize");
        return "auto role";
    }

	// 进入控制器之前对数据进行过滤
	@PostMapping("/getPreFilter")
    @PreAuthorize("hasRole('ROLE_管理员')")
    @PreFilter(value = "filterObject.id%2==0")
    @ResponseBody
    public List<UserInfo> getTestPreFilter(@RequestBody List<UserInfo> list){
        list.forEach(t-> {
            System.out.println(t.getId()+"\t"+t.getName());
        });
        return list;
    }

	// 表达式中的 filterObject 引用的是方法返回值 List 中的某一个元素
    @GetMapping("/getAll")
    @PreAuthorize("hasRole('ROLE_role')")
    @PostFilter("filterObject.name == 'admin1'")
    @ResponseBody
    public List<UserInfo> getAllUser(){
        ArrayList<UserInfo> list = new ArrayList<>();
        list.add(new UserInfo(1,"admin1","6666"));
        list.add(new UserInfo(2,"admin2","888"));
        return list;
    }

3.6、登出操作

在进行登出时,只需要在配置类上加入登出对应的配置即可。在访问/logout路由的时候,会进行登出操作,销毁session,页面跳转到/index页面

	http.logout().logoutUrl("/logout").logoutSuccessUrl("/index").permitAll();

3.7、RememberMe 自动登录

在进行登录验证的时候,会通过RememberMeAuthenticationFilter过滤器。而在通过过滤器之后,会获取token值进行校验,而在之前的登录过程当中会通过JdbcTokenRepositoryImpl实现类,该类主要是对每次登录之后将生成的token值写入到数据库当中,而后续的自动登录的时候,会获取浏览器的token和数据库当中存的token进行比较验证。

在这里插入图片描述
新增表表结构,用来记录登录信息以及token,以及springboot项目对数据库的一些相关配置。

CREATE TABLE `persistent_logins` (
 `username` varchar(64) NOT NULL,
 `series` varchar(64) NOT NULL,
 `token` varchar(64) NOT NULL,
 `last_used` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE
CURRENT_TIMESTAMP,
 PRIMARY KEY (`series`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

这里对于表数据的操作,在JdbcTokenRepositoryImpl实现类当中都有,也不需要我们自己去对数据库进行操作,我们只需要在配置类当中加入相关的配置即可。如下:(部分代码)

    @Autowired
    private DataSource dataSource;

    @Bean
    public PersistentTokenRepository persistentTokenRepository() {
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        // 赋值数据源
        jdbcTokenRepository.setDataSource(dataSource);
        // 自动创建表,第一次执行会创建,以后要执行就要删除掉!
        // jdbcTokenRepository.setCreateTableOnStartup(true);
        return jdbcTokenRepository;
    }

	// 在configure方法当中指定rememberMe
	and().rememberMe().tokenRepository(persistentTokenRepository())
		.tokenValiditySeconds(60).userDetailsService(userDetailsService)

并且在登录页面上可以加上一个checkbox复选框,用来标识rememberMe,这里的name的值也必须是remember-me。

<input type="checkbox"name="remember-me"title="记住密码"/><br/>

之后进行测试,在进行登录的时候,当认证通过之后,会将的搭配的token值写入到数据库当中,并且会表示该token的过期时间。而当这个时候,我们关掉浏览器之后直接访问路由(非登录路由),还是可以直接进行访问的,这是因为我们的请求当中还带上了token,token校验通过之后还是可以直接进去的,而当token过期之后再进行访问,又会直接给转到登录页面。

3.8、CSRF 跨域

跨站请求伪造,通常缩写为 CSRF 或者 XSRF, 是一种挟制用户在当前已登录的 Web 应用程序上执行非本意的操作的攻击方法。跟跨网站脚本(XSS)相比,XSS利用的是用户对指定网站的信任,CSRF 利用的是网站对用户网页浏览器的信任。

从 Spring Security 4.0 开始,默认情况下会启用 CSRF 保护,以防止 CSRF 攻击应用程序,Spring Security CSRF 会针对 PATCH,POST,PUT 和 DELETE 方法进行防护。

可以使用一下语句进行关闭跨域检查:

http.csrf().disable();

4、SpringSecurity 微服务权限方案

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Modify_QmQ

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值