提示文章写完后,目录可以自动生成如何生成参考右边的帮助文档


前言

spring security的超详细配置和使用攻略,包括从登录校验开始到不同页面不同功能权限管理,包括了整合thymeleaf框架用户登录信息持久化处理、csrf防护web安全


一、SpringSecurity什么

SpringSecurity是SpringBoot支持高度自定义安全框架利用了SpringDI和SpringAOP的设计,提供了声明安全访问控制功能,减少了大量为了安全配置所写的重复代码

这里安全主要是指用户权限管理,即不同的用户访问不同页面菜单,如果没有相关权限,则不能访问相关页面

二、所需数据库设计

所有用户数据都是保存数据库中,权限角色)也应该保存数据库中,用户与权限是多对多的关系,就需要一张中间表,所以现在需要三张表:用户表(user),权限表(role),权限用户中间表(role_user_link)。

web项目中,什么是权限的具体体现?就是各种url,不同的权限可以访问url也不同,url与权限也是多对多的关系,所以又加了两张表:url表(url),url与权限中间表(role_url_link)。

基础需要一共是五张表:

在这里插入图片描述
但是一般用不到url表,暂时我是没有用到,所以还是基础用户角色三张表。

三、使用步骤

新建一个spring boot项目导入基本webmysqlmybatismybatisplusthymeleafspring boot testlombok依赖快速创建实体类、Dao和Service

1.添加依赖

代码如下示例):

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

2.创建SecurityConfig类

与之前的security教程,配置类会这样写:

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {}

随着WebSecurityConfigurerAdapter的过时,创建SecurityConfig类也变得简单起来:

@EnableWebSecurity
@Configuration
public class SecurityConfig {}

就只是我们自己新建一个类,加上配置类注解就好,其中的方法下面会讲到。

3. 往配置类中方法实现登录验证

直接代码

@EnableWebSecurity
@Configuration
public class SecurityConfig {
	@Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
   		httpSecurity.csrf().disable();
        httpSecurity.authorizeRequests()
                .anyRequest().authenticated()//所有请求,都需要登录状态校验
                .and()
                .formLogin();
        return httpSecurity.build();
    }
}

先将csrf跨站请求攻击关闭,后面会讲解如何配置。

往SecurityConfig类中添加上面的方法,并标注为Bean,即可实现简单登录,即使自己没有页面

默认的urllogin我们上面配置了拦截所有请求,所以输入任意url都会被拦截然后跳到该登录页面自带页面,好像还不错:

SpringSecurity自带的登录页面

SpringSecurity自带了登录页面默认用户名username密码运行项目后,会在控制台打印出来,直接复制即可
自动生成的密码
如果嫌麻烦,可以直接在application.yml指定security默认用户名和密码,但不建议这么做,因为这个用不到,毕竟用户数据要从数据库中取

四、自定义登录功能认证功能

这次要开始动真格的了,不仅要从数据库取数据,还要走自己的登录页面跳转首页

1.从数据库获取用户数据

这个老生常谈,不知道在做项目过程中做了多少注册登录了。几乎都是从映射实体类、Dao层、Service层、Controller层等。

security要实现数据库获取用户数据也与以往步骤差不多,需要自己写Dao层,但只需要将查询到的用户实体对象交给SecurityService实现即可,会自动我们设定好的配置,不需要我们自己写controller了。

首先,新建一个SecurityService类,这个类需要实现UserDetailsService接口然后需要实现其中的一个方法

@Service
public class SecurityService implements UserDetailsService {
    private final UserService userService;

    public SecurityService(UserService userService) {
        this.userService = userService;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userService.getOne(new QueryWrapper<User>()
                .eq("username", username));
        
        if (user == null) {
            throw new UsernameNotFoundException("user is not found");
        }
        //return这么一长串是因为我的user实体类名字是User,与org.springframework.security.core.userdetails.User重名了
        return new org.springframework.security.core.userdetails.User(
                username,
                user.getPassword(),
                AuthorityUtils.commaSeparatedStringToAuthorityList("")//该参数暂时用不到,无意义,参数随便填
        );
    }
}

这里我为了偷懒使用的是mybatis-plus总体逻辑便是从数据库中查出对应user对象,并返回userDetails.User,切记不是直接返回实体类对象

再次到登陆页面之后,会发现即使输入正确数据表中的用户名和密码,控制台报错
There is no PasswordEncoder mapped for the idnull”:

ERROR 24820 --- [nio-8080-exec-8] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception

java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"

这是由于Security出于安全,需要将密码加密后比对。因此我们还需要在前面SecurityConfig类中配置密码加密器:

    /**
     * 密码加密器,会把客户端传来的密码进行加密然后数据库中的密码做对比,要求数据库中的密码也是加密过的
     * 如果没有加密器,spring security会报出异常:There is no PasswordEncoder mapped for the id "null"
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

再次登录,用户名密码正确结果报出下面的异常Encoded password does not look like BCrypt

WARN 14588 --- [nio-8080-exec-4] o.s.s.c.bcrypt.BCryptPasswordEncoder     : Encoded password does not look like BCrypt

并且登录失败
提示密码错误
这是因为,程序内部使用的是加密后的密码与数据库中的明文密码进行比对,当然是错误的,这也是刚接触security常见错误解决方法有如下两种:
第一种(不推荐),在我们取出user对象后,在SecurityService中修改userDetails.User传参时候,将密码进行加密

		//return这么一长串是因为我的user实体类名字是User,与org.springframework.security.core.userdetails.User重名了
        return new org.springframework.security.core.userdetails.User(
                username,
                new BCryptPasswordEncoder().encode(user.getPassword()),
                AuthorityUtils.commaSeparatedStringToAuthorityList("")//无意义,参数随便填
        );

第二种,在数据库存储的密码就是加密后的密码。

这里要求整个项目中注册修改密码后都是存储密文,那我们做demo数据还是来自我们自己添加,一个个单元测试输入明文后再复制密文到数据库也是一个获取密文可行的方法

这里推荐一个网站可以获取多种加密方式的密文:
在线加密

在做单元测试发现相同的明文每次加密密文都不同,但依旧能验证成功,这里推荐一个文章,感兴趣的朋友可以看看为什么密文不同还能比对成功

2. 自定义登录页面

要使用自己写的登录页面,只需要如下几步即可

  1. templates文件夹下添加自己的登录页面
  2. 在Controller层中配置登录页面跳转显示逻辑
  3. 在SecurityConfig类中相关配置即可

SecurityConfig配置类中代码如下:

	httpSecurity.formLogin()
                .loginProcessingUrl("/toLogin")
                .loginPage("/myLogin")

loginProcessingUrl代表的是页面中form表单action,一旦接收到该url,就会跳转登陆页面。
loginPage代表的是我们自定义登陆页面。当然不要忘记了在controller中配置。

3. 登录成功逻辑

登陆成功后一般就要跳转别的页面,security提供了三个登录成功后的跳转方式,分别是:

httpSecurity.formLogin()
            .defaultSuccessUrl()//默认
            .successForwardUrl()//成功跳转
            .successHandler()//可选默认,也可自定义的

defaultSuccessUrl(“/url”, alwaysUse)

defaultSuccessUrl有两个实现,一个只需要一个url参数,另一个还要一个boolean类型参数

defaultSuccessUrl默认登录成功后重定向,如果alwaysUse参数设置false,则会跳转到登录前的页面,而不是url参数的页面。设置true则会强制为url参数页面。不写为false

successForwardUrl(“/url”)

successForwardUrl登录成功后内部请求转发,url地址不会变,所以会显示formaction,即loginProcessingUrl拦截地址

successHandler()

如果参数是:new ForwardAuthenticationSuccessHandler("/index")登录成功后会是内部请求转发,与successForwardUrl()相同。但可以自定义实现了AuthenticationSuccessHandler接口的类,然后实现重定向,如:

public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    private final String url;

    public MyAuthenticationSuccessHandler(String url) {
        this.url = url;
    }

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        response.sendRedirect(url);
    }
}

使用自定义参数:

.successHandler(new MyAuthenticationSuccessHandler("/index"))

4. 登录失败逻辑

与登录成功类似,登录失败也有三个类似的处理方式

httpSecurity.formLogin()
                .failureUrl()
                .failureForwardUrl()
                .failureHandler()

主要讲一下failureUrlfailureForward区别和使用注意事项
① failureUrl

defaultSuccessUrl类似,是通过重定向的方式进行跳转。

但是由于是失败后的跳转,是连最基本的登录认证没有通过的,所以是没有任何权限的,如果重定向的页面有权限限制则会因为没有权限而回到登录页面,这点需要格外注意

② failureForwardUrl

successForwardUrl类似,是通过服务器内部转发实现跳转的不再多说。

③ failureHandler()

successHandler类似,不再赘述。

5. 自定义表单的键名

登录功能数据来源前端页面发送来的表单数据,SpringSecurity默认的键名(form表单中name属性)分别是:username和password。

如果想要使用自定义键名,只需要在前端表单写好后,再在SecurityConfig中添加如下代码即可

				.usernameParameter("username")
                .passwordParameter("password")

这里的username和password就可以改为你自定义的键名。

6. rememberMe功能

rememberMe顾名思义就是记住我功能,要开启此功能只需要在securityConfig配置类中添加代码即可

	httpSecurity.rememberMe()
                .tokenValiditySeconds(60*60*24*14)//cookie过期时间单位秒,默认2周,过期数据库中的记录并不会删除
                .rememberMeCookieName("rememberMe")
                .userDetailsService(securityUserDetailsService)//登录逻辑交给哪个对象
                .tokenRepository(persistentTokenRepository)//持久层对象
                .rememberMeParameter("rememberMe")

下面逐个了解分别配置了什么东西。

@Bean
    public PersistentTokenRepository persistentTokenRepository(DataSource dataSource) throws SQLException {
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        jdbcTokenRepository.setDataSource(dataSource);
        //根据表名查找
        ResultSet resultSet = Objects.requireNonNull(jdbcTokenRepository.getDataSource()).getConnection().getMetaData().getTables(null, null, "persistent_logins", new String[]{"TABLE"});
        //如果已经存在则跳过,如不存在则创建
        if (resultSet == null || !resultSet.next()) {
            jdbcTokenRepository.setCreateTableOnStartup(true);
        }
        return jdbcTokenRepository;
    }

然后利用spring自动注入实例化persistentTokenRepository,再在需要的地方使用。
构造注入

private final PersistentTokenRepository persistentTokenRepository;
public SecurityConfig(PersistentTokenRepository persistentTokenRepository) {
        this.persistentTokenRepository = persistentTokenRepository;
    }

需要注意的是,由于关于persistentTokenRepository的Bean是直接写在该配置类中的,如果要在该配置类中自动注入自己的类,会有循环依赖的问题,所以需要在构造器的参数上加上@Lazy注解

private final PersistentTokenRepository persistentTokenRepository;
public SecurityConfig(@Lazy PersistentTokenRepository persistentTokenRepository) {
        this.persistentTokenRepository = persistentTokenRepository;
    }

循环依赖可以参考篇文章循环依赖问题

配置好以后,启动项目就会自动检查数据库中是否名为persistent_logins的表,如果没有则会自动创建,如果有,则会跳过创建。

表中的数据即使用户信息过期了,也不会删除。
如果未过期,则更新最近登陆时间。
如果过期了再登录会新插入一条记录

7. 退出登录功能

退出功能的配置代码如下:

	httpSecurity.logout()//会清除rememberMe该用户的登录记录
                .logoutUrl("/exit");//要与前端退出登录的url一致

如果开启了rememberMe功能,并且配置了持久层连接数据库,那么退出登录时会将该用户的记录全部删除,无论是过期还是未过期的记录

logout的其余配置,包括退出成功后、退出失败后等等与登录的配置大同小异,这里不再赘述。

五、授权功能

1. 取得用户角色和权限

还记得在前面4.1中的SecurityService中根据用户名字获取user对象并返回UserDetails的方法吗?

//return这么一长串是因为我的user实体类名字是User,与org.springframework.security.core.userdetails.User重名了
       return new org.springframework.security.core.userdetails.User(
                username,
                user.getPassword(),
                AuthorityUtils.commaSeparatedStringToAuthorityList("")//该参数暂时用不到,无意义,参数随便填
        );

其中返回的对象中有个参数是 AuthorityUtils.commaSeparatedStringToAuthorityList("") ,当时用不到所以是无意义的,现在权限校验需要用到了。

首先,我们从数据库中获取到user对象后,可以根据userId查找role_user_link找到roleId,再根据roleId找到role。注意到该参数是一个集合,所以查找结果应该是个List<String>,即使一个用户可能只有一个角色。

根据数据库设计不同,可能是权限与用户绑定,那么一个用户就可以有多个权限,使用List<Strign>是不是就合理多了?

当然也有的数据库设计,既有角色,又有权限,即:用户表、用户角色中间表、角色表、角色权限中间表、权限表。这样可以即查出角色又查出权限,然后一起塞到AuthorityList中。

下面是完整的SecurityService类:

@Service
public class SecurityService implements UserDetailsService {
    private final UserService userService;
    private final RoleUserLinkService roleUserLinkService;
    private final RoleService roleService;

    public SecurityService(UserService userService, RoleUserLinkService roleUserLinkService, RoleService roleService) {
        this.userService = userService;
        this.roleUserLinkService = roleUserLinkService;
        this.roleService = roleService;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userService.getOne(new QueryWrapper<User>()
                .eq("username", username));

        if (user == null) {
            throw new UsernameNotFoundException("user is not found");
        }

        List<String> roles = new ArrayList<>();
        roleUserLinkService.listObjs(new QueryWrapper<RoleUserLink>()
                .select("role_id")
                .eq("user_id", user.getId()))
                .forEach(roleId -> roles.addAll(roleService.listObjs(new QueryWrapper<Role>()
                        .select("role")
                        .eq("id", roleId)).stream().map(Object::toString).collect(Collectors.toList())));

        List<SimpleGrantedAuthority> authorityList = new ArrayList<>();
        //这里其实是权限,而不是角色
        roles.forEach(role -> authorityList.add(new SimpleGrantedAuthority(role)));
        //角色应该这样写,我这里没有权限,只有角色。所以就都添加了,去掉ROLE_就是一样的
        roles.forEach(role -> authorityList.add(new SimpleGrantedAuthority("ROLE_" + role)));

        //return这么一长串是因为我的user实体类名字是User,与org.springframework.security.core.userdetails.User重名了
        return new org.springframework.security.core.userdetails.User(
                username,
                //在线BCrypt加密 https://www.bejson.com/encrypt/bcrpyt_encode/
                user.getPassword(),
                authorityList
        );
    }
}

我的数据库设计是三张表,即:用户表、角色表、中间表。所以角色充当了权限和角色两种。

代码可见,anthorityList.add()中针对角色有特定的前缀“ROLE_”,如果要插入role,想要按照role进行验证,这个前缀是必须的。

2. 配置校验规则

现在我们已经取到了登录用户的角色和权限信息,下面就是去配置类中指定哪些url需要哪些权限或角色才能访问:

httpSecurity.authorizeRequests()
                .antMatchers("/myLogin").permitAll()//myLogin所有人都可以访问
                .antMatchers("/index").permitAll()
                .antMatchers("/studentIndex").hasAnyRole("student", "admin")//按照角色校验,但是不加 ROLE_ 的前缀,双引号
                .antMatchers("/teacherIndex").hasAnyAuthority("teacher", "admin")//按照权限校验,双引号
                .antMatchers("/adminIndex").hasAnyAuthority("admin")
                .antMatchers("/teacherIndex").access("@myPowerServiceImpl.isPowerEnabled(request, authentication)")//自定义拦截,参数只能写request,不能写httpServletRequest,不知道为什么,也不知request哪里来的
                .anyRequest().authenticated()//其他请求需要登录状态校验
  1. authorizeRequests()开启request验证
  2. antMatchers()指定验证的url,后面跟上对该url的验证规则
  3. permitAll()表示该url所有人都可以访问,一般用于公共页面,登录、首页错误
  4. hasAnyRole、hasRole:该url只有这些角色才能访问,不同的是anyRole后面可以跟多个角色,role后面只能跟一个角色。这里角色不必加我们获取到角色时添加的前缀ROLE_。
  5. hasAnyAuthority、hasAuthority:该url只有拥有这些权限才能访问,any带与不带的区别同上。
  6. access()下面会讲到
  7. anyRequest():其他请求
  8. authenticated():处于登陆状态即可访问

所以上述代码翻译为人话就是:

myLogin、index所有人都可以访问,无论登不登陆;
studentIndex 角色为 studentadmin 可以访问;
teacherIndex 角色为 teacher 和 admin 可以访问;
adminIndex 角色为 admin 可以访问;
其他请求,只要成功登录了就可以访问。

3. 自定义拦截规则

有的项目中会要求权限模糊校验,与权限对比有一部分通过校验即可访问,比如:有权限名为 class:manger:student,项目要求只要有class就可以通过校验。
这个例子尽管不恰当,但这种要求可以通过自定义access()中的参数实现:
我们先定义一个接口类

public interface MyPowerService {
    Boolean isPowerEnabled(HttpServletRequest request, Authentication authentication);
}

然后实现它:

@Service
public class MyPowerServiceImpl implements MyPowerService {
    @Override
    public Boolean isPowerEnabled(HttpServletRequest request, Authentication authentication) {
        Object user = authentication.getPrincipal();
        if (user instanceof UserDetails) {
            UserDetails userDetails = (UserDetails) user;
            Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();
            //角色需要写ROLE_,权限不用,我这里没有权限,只有角色。所以就都添加了,去掉ROLE_就是一样的
//            boolean enabled1 = authorities.contains(new SimpleGrantedAuthority("ROLE_student"));
            boolean enabled2 = authorities.contains(new SimpleGrantedAuthority("teacher"));
            boolean enabled3 = authorities.contains(new SimpleGrantedAuthority("ROLE_admin"));

            return enabled2 || enabled3;
        }
        return false;
    }
}

然后access()中通过@调用即可:

.antMatchers("/teacherIndex").access("@myPowerServiceImpl.isPowerEnabled(request, authentication)")//自定义拦截,参数只能写request,不能写httpServletRequest,不知道为什么,也不知request哪里来的

注意isPowerEnabled()中的第一个参数,虽然实现类中定义时是httpservlet,但是只能写request

4. 通过注解方式实现授权

如果觉得上面的方式过于麻烦,且写起来又臭又长,那么下面就是喜闻乐见的注解方式了。

使用注解的方式添加授权步骤如下:

  1. 在SpringBoot启动类上添加如下注解开启注解校验功能:
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true, proxyTargetClass = true)
  1. controller层的请求处理方法上面按照格式添加注解即可,例如
@Secured({"ROLE_student","ROLE_admin"})//作用相当于SecurityConfig中的securityFilterChain中的hasAnyRole,但是需要加ROLE_
    @RequestMapping("/studentIndex")
    public String studentIndex() {
        return "student/studentIndex";
    }

    @Secured({"ROLE_teacher","ROLE_admin"})
    @PreAuthorize("hasAnyAuthority('teacher', 'admin')")//作用相当于SecurityConfig中的securityFilterChain中的hasAnyAuthority,但里面单引号
    @RequestMapping("/teacherIndex")
    public String teacherIndex() {
        return "teacher/teacherIndex";
    }

    @Secured({"ROLE_admin"})
    @PreAuthorize("hasAnyAuthority('admin')")//在进行操作前就拦截
//    @PostAuthorize("hasAnyAuthority('admin')")//在进行操作后才进行校验
    @RequestMapping("/adminIndex")
    public String adminIndex() {
        return "admin/adminIndex";
    }

注解格式介绍

  1. @Secured({“ROLE_student”,“ROLE_admin”})
    跟SecurityConfig类中的hasAnyRole很像,只是hasAnyRole()参数不必加”ROLE_”前缀。
  2. @PreAuthorize(“hasAnyAuthority(‘teacher’, ‘admin’)”)
    作用相当于SecurityConfig中的securityFilterChain中的hasAnyAuthority,但里面单引号
  3. @PostAuthorize(“hasAnyAuthority(‘admin’)”)
    作用相似于SecurityConfig中的securityFilterChain中的hasAnyAuthority,但里面单引号
  4. @PreAuthorize和@PostAuthorize的区别
    @PreAuthorize是在执行处理请求的方法之前进行授权校验,如果不通过则不会执行方法。
    @PostAuthorize则是在执行过方法之后才进行权限校验,如果不通过则不会有反馈,一般用于特殊需求

六、CSRF跨站伪造请求攻击防护

1. 什么跨站伪造请求攻击?

跨站伪造请求攻击
用户在客户端浏览器访问A站,保存登陆信息后,又去浏览网站B,网站B偷偷获取cookie中的用户信息然后拿着该信息去访问网站A,这个过程用户并没有在网站A做任何操作,一系列操作都是网站B拿着用户信息去伪造的,这就是跨站伪造请求攻击。

2. csrf原理

3. csrf的使用

csrf的前端taken格式部分较为固定

<form method="post" id="userForm" action="/toLogin"><!-- action都会被拦截 -->
    <input type="hidden" name="_csrf" th:if="_csrf" th:value="${_csrf.token}"><!-- 每个需要做验证的页面都要写 -->
  <label>
    账号<input type="text" name="username"/>
  </label>
  <label>
    密码:<input type="password" name="password"/>
  </label>
    <label>
        <input type="checkbox" name="rememberMe" value="true"/>记住?
    </label>
    <button type="submit" name="login">登录</button>
</form>

其中name固定为 _csrf,搭配thymeleaf使用。
后端:仅需这一句即可:

httpSecurity.csrf();

如果开启了该功能,几乎每个页面都必须有这样的一个存储taken的地方,否则会报出权限不足的异常

总结

本文仅仅简单介绍了SpringSecurity的使用。
第一次写,如有错误欢迎指出,欢迎给出宝贵的建议和意见。

原文地址:https://blog.csdn.net/qq_42713539/article/details/129721027

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任

如若转载,请注明出处:http://www.7code.cn/show_50437.html

如若内容造成侵权/违法违规/事实不符,请联系代码007邮箱suwngjj01@126.com进行投诉反馈,一经查实,立即删除!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注