Springboot 整合 Spring Security

作者: adm 分类: java 发布时间: 2022-07-14

Springboot 整合 Spring Security
Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。

项目最终目录结构

一.Spring Security 引入
1.pom 文件引入 spring security 依赖

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

2.启动项目,并在浏览器访问项目,出现如下登录页,说明 security 已引入成功

这是 security 自带的默认登录页面,项目的操作需要登录后才可进行操作
用户名默认为 user ,密码可在项目启动日志中可进行查看

二.自定义用户
Spring Security 支持三种用户配置方式

1.通过配置文件方式
在 application.yml 中添加用户

spring:
  # security 
  security:
    user:
      #用户名
      name: ppp
      #密码
      password: 123456
      #权限
      roles: USER

2.基于内存方式

	//认证
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("aaa").password("123").roles("USER")
                .and()
                .withUser("admin").password("123").roles("ADMIN","USER")
                .and()
                .withUser("bbb").password("123").roles("ADMIN");
    }

3.基于数据库方式

真正使用中,用得最多的还是基于数据库方式,所以这里主要讲解第三种方式。
为方便操作数据库,项目中已整合 mybatis-plus。如小伙伴未整合有需要,可参考我以前发布过的文章“Springboot 整合 mybatis-plus”。

数据库中创建用户表

create table tbl_user
(
    id        int auto_increment
        primary key,
    user_code varchar(200)  not null,
    password  varchar(2000) not null,
    role      varchar(2000) null
);

向表中添加用户数据

创建 security 授权配置文件 WebSecurityConfig

import com.example.demo.web.service.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;

import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;


import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;


@Configuration
@EnableWebSecurity
//开启注解功能
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true) // 启用方法级别的权限认证
public class WebSecurityConfig {
    @Autowired
    private UserDetailsServiceImpl userDetailsServiceImpl;

    /*@Autowired
    private JwtAuthError jwtAuthError;
    // jwt 校验过滤器,从 http 头部 Authorization 字段读取 token 并校验
    @Bean
    public JwtAuthFilter authFilter() throws Exception {

        return new JwtAuthFilter();
    }*/

    /**
     * 密码明文加密方式配置 * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        //无加密
        //return NoOpPasswordEncoder.getInstance();
        return new BCryptPasswordEncoder();
    }

    /**
     * 获取AuthenticationManager(认证管理器),登录时认证使用 * @param authenticationConfiguration * @return * @throws Exception
     */
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {

        return authenticationConfiguration.getAuthenticationManager();
    }

    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
               // 基于 token,不需要 csrf
                .csrf().disable()
                // 基于 token,不需要 session
                //.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                 // 设置 jwtAuthError 处理认证失败、鉴权失败
                // .exceptionHandling().authenticationEntryPoint(jwtAuthError).accessDeniedHandler(jwtAuthError).and()
               // 下面开始设置权限
                .authorizeRequests(authorize -> authorize
                // 请求放开
                                .antMatchers("/static/**", "/login", "/hello/").permitAll()
                                .antMatchers("/demo/test").hasAuthority("admin")
                                .antMatchers("/demo/test1").hasAnyAuthority("user", "op")
                // 其他地址的访问均需验证权限
                                .anyRequest().authenticated()


                )
                .formLogin()
                .loginPage("/login")
                //登陆请求处理接口
                .loginProcessingUrl("/login")
                //用户名文本框 name属性
                .usernameParameter("username")
                //密码文本框 name属性
                .passwordParameter("password")
                //如果登录成功会跳转到"/hello"
                .defaultSuccessUrl("/index")
                //登录成功处理
//                .successHandler((req, resp, auth) -> {
//
//                })
                //如果登录失败会跳转到"/hello"
                .failureForwardUrl("/hello")
                //登录失败处理
//                .failureHandler((req, resp, e) -> {
//
//                })
                .and()
                //登出配置
                .logout()
                .logoutUrl("/admin/logout") //指定登出的地址,默认是"/logout"
                .logoutSuccessUrl("/hello")   //登出后的跳转地址
                .clearAuthentication(true)  //清楚身份信息
                .invalidateHttpSession(true)  //session 失效,默认为true
                .deleteCookies("usernameCookie","urlCookie") //在登出同时清除cookies

//                //自定义LogoutSuccessHandler,在登出成功后调用,如果被定义则logoutSuccessUrl()就会被忽略
//                .logoutSuccessHandler((req, resp, auth) -> {//注销成功处理
//                    resp.sendRedirect("/login_page");              //跳转到自定义登陆页面
//                })

                //添加自定义的LogoutHandler,默认会添加SecurityContextLogoutHandler
//                .addLogoutHandler((req, resp, auth) -> {//注销处理
//
//                })

                .and()

                //自定义403页面
                .exceptionHandling()
                .accessDeniedPage("/403")
                .and()
                // 添加 JWT 过滤器,JWT 过滤器在用户名密码认证过滤器之前
                //.addFilterBefore(authFilter(), UsernamePasswordAuthenticationFilter.class)

                // 认证用户时用户信息加载配置,注入springAuthUserService
                .userDetailsService(userDetailsServiceImpl)

                .build();

    }


    /**
     * 配置跨源访问(CORS) * @return
     */
    @Bean
    CorsConfigurationSource corsConfigurationSource() {

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());
        return source;
    }


}

hasRole() 当前用户是否拥有指定角色。
hasAnyRole() 多个角色是一个以逗号进行分隔的字符串。如果当前用户拥有指定角色中的任意一个则返回true。

hasAuthority() 等同于hasRole
hasAnyAuthority() 等同于hasAnyRole

hasRole 的处理逻辑和 hasAuthority 类似,不同的是,hasRole 这里会自动给传入的字符串加上 ROLE_ 前缀,所以在数据库中的权限字符串需要加上 ROLE_ 前缀。即数据库中存储的用户角色如果是 ROLE_admin,这里就是 admin。
创建 UserDetailsServiceImpl 自定义身份认证

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.example.demo.web.entity.AdminUserEntity;
import com.example.demo.web.mapper.AdminUserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    private AdminUserMapper adminUserMapper;

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

        QueryWrapper wrapper = new QueryWrapper();
        wrapper.eq("username", username);
        // MOCK 模拟从数据库 根据用户名查询用户
        AdminUserEntity account = adminUserMapper.selectOne(wrapper);
        if (account == null) {
            throw new UsernameNotFoundException("用户不存在");
        }

        //用户权限
        String[] permissionArray = account.getRole().split(",");
        //return User.withUsername(account.getUsername()).password(new BCryptPasswordEncoder().encode(account.getPassword())).authorities(new SimpleGrantedAuthority(permissionArray)).build();
        return User.withUsername(account.getUsername()).password(account.getPassword()).authorities(permissionArray).build();
    }

}

创建 AdminUserEntity

@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("yz_admin_user")
public class AdminUserEntity {

    private Integer id;

    private String username;

    private String password;

    private String role;

}

创建 AdminUserMapper

public interface AdminUserMapper extends BaseMapper {
}

测试启动项目,在登录页输入数据库中的用户名和密码登录

登录成功,并跳转到项目主页
登录 java用户,因为 java用户只有 USER 权限,所以访问 demo/test 时是没权限的,所以出现 403 无权限错误页面

security 连接数据库登录完成。

三.加密策略
刚刚的写法,用户的密码是没有进行加密的。
真实项目使用中,为了保护用户的信息安全,数据库中的密码是必须进行加密保存。

更改 security 加密策略

	//密码加密方式
    @Bean
    PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

修改数据库用户密码

Spring Security 提供了丰富的加密策略

配置完成,启动项目测试

授权配置完成

四.自定义登录页
在项目中添加登录页 html,以及访问 controller 。

controller

package com.example.demo.common.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.servlet.ModelAndView;

@Slf4j
@Controller
public class CommonController {

    @GetMapping(value = "hello")
    public ModelAndView hello(){
        return new ModelAndView("login");
    }

    @GetMapping(value = "index")
    public ModelAndView index(){
        return new ModelAndView("index");
    }

}

remember-me 记住我设置
数据库添加表 persistent_logins,用于存放token

create table persistent_logins
(
    username  varchar(64)                         not null,
    series    varchar(64)                         not null
        primary key,
    token     varchar(64)                         null,
    last_used timestamp default CURRENT_TIMESTAMP not null
);

security 配置文件注入数据源 DataSource 和配置 TokenRepository ,需要和数据库进行交互

//注入数据源
@Autowired
private DataSource dataSource;

private PersistentTokenRepository persistentTokenRepository(){
    JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
    jdbcTokenRepository.setDataSource(dataSource);
    //第一次启动的时候自动建表
//        jdbcTokenRepository.setCreateTableOnStartup(true);
    return jdbcTokenRepository;
    }

security 配置文件中 configure(HttpSecurity http) 添加配置

.and()
//记住我配置
.rememberMe()
//记住我复选框框 name属性
.rememberMeCookieName("remember-me")
.tokenRepository(persistentTokenRepository())
//设置有效时长,单位秒
.tokenValiditySeconds(60)
.userDetailsService(myUserDetailsService)

最终 security 配置文件

package com.example.demo.web.config;


import com.example.demo.web.service.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;

import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;


import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;


@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) // 启用方法级别的权限认证
public class WebSecurityConfig {
    @Autowired
    private UserDetailsServiceImpl userDetailsServiceImpl;

    /*@Autowired
    private JwtAuthError jwtAuthError;
    // jwt 校验过滤器,从 http 头部 Authorization 字段读取 token 并校验
    @Bean
    public JwtAuthFilter authFilter() throws Exception {

        return new JwtAuthFilter();
    }*/

    /**
     * 密码明文加密方式配置 * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        //无加密
        //return NoOpPasswordEncoder.getInstance();
        return new BCryptPasswordEncoder();
    }

    /**
     * 获取AuthenticationManager(认证管理器),登录时认证使用 * @param authenticationConfiguration * @return * @throws Exception
     */
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {

        return authenticationConfiguration.getAuthenticationManager();
    }

    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
               // 基于 token,不需要 csrf
                .csrf().disable()
                // 基于 token,不需要 session
                //.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                 // 设置 jwtAuthError 处理认证失败、鉴权失败
                // .exceptionHandling().authenticationEntryPoint(jwtAuthError).accessDeniedHandler(jwtAuthError).and()
               // 下面开始设置权限
                .authorizeRequests(authorize -> authorize
                // 请求放开
                                .antMatchers("/static/**", "/login", "/hello/").permitAll()
                                .antMatchers("/demo/test").hasAuthority("admin")
                                .antMatchers("/demo/test1").hasAnyAuthority("user", "op")
                // 其他地址的访问均需验证权限
                                .anyRequest().authenticated()


                )
                .formLogin()
                .loginPage("/hello")
                //登陆请求处理接口
                .loginProcessingUrl("/login")
                //用户名文本框 name属性
                .usernameParameter("username")
                //密码文本框 name属性
                .passwordParameter("password")
                //如果登录成功会跳转到"/hello"
                .defaultSuccessUrl("/index")
                //登录成功处理
//                .successHandler((req, resp, auth) -> {
//
//                })
                //如果登录失败会跳转到"/hello"
                .failureForwardUrl("/hello")
                //登录失败处理
//                .failureHandler((req, resp, e) -> {
//
//                })
                .and()
                //登出配置
                .logout()
                .logoutUrl("/admin/logout") //指定登出的地址,默认是"/logout"
                .logoutSuccessUrl("/hello")   //登出后的跳转地址
                .clearAuthentication(true)  //清楚身份信息
                .invalidateHttpSession(true)  //session 失效,默认为true
                .deleteCookies("usernameCookie","urlCookie") //在登出同时清除cookies

//                //自定义LogoutSuccessHandler,在登出成功后调用,如果被定义则logoutSuccessUrl()就会被忽略
//                .logoutSuccessHandler((req, resp, auth) -> {//注销成功处理
//                    resp.sendRedirect("/login_page");              //跳转到自定义登陆页面
//                })

                //添加自定义的LogoutHandler,默认会添加SecurityContextLogoutHandler
//                .addLogoutHandler((req, resp, auth) -> {//注销处理
//
//                })

                .and()

                //自定义403页面
                .exceptionHandling()
                .accessDeniedPage("/403")
                .and()

                //记住我配置
                .rememberMe()
                //记住我复选框框 name属性
                .rememberMeCookieName("remember-me")
                .tokenRepository(persistentTokenRepository())
                //设置有效时长,单位秒
                .tokenValiditySeconds(60)
                .and()
                // 添加 JWT 过滤器,JWT 过滤器在用户名密码认证过滤器之前
                //.addFilterBefore(authFilter(), UsernamePasswordAuthenticationFilter.class)

                // 认证用户时用户信息加载配置,注入springAuthUserService
                .userDetailsService(userDetailsServiceImpl)

                .build();

    }


    /**
     * 配置跨源访问(CORS) * @return
     */
    @Bean
    CorsConfigurationSource corsConfigurationSource() {

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());
        return source;
    }


}


五.security 注解权限控制
想要使用 security 注解,需要在启动类或配置文件中添加注解 @EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true) 开启security 注解功能

其中:
prePostEnabled = true 开启 @PreAuthorize,@PostAuthorize,@PreFilter,@PostFilter 注解

securedEnabled = true 开启 @Secured 注解

1). @Secured

@GetMapping("/test")
@Secured({"ADMIN","USER"})
public String test() {
    return "hello";
}

说明:拥有ADMIN或者USER角色的用户都可以方法 test() 方法。

2). @PreAuthorize
在方法执行前进行权限验证
与 @Secured 类似,但是 @PreAuthorize 支持 SpEL表达式

@GetMapping("/test")
@PreAuthorize("hasAuthority('ADMIN','USER')")
public String test() {
    return "hello";
}

说明:拥有normal或者admin角色的用户都可以方法 test()方法。

此时如果我们要求用户必须同时拥有normal和admin的话,可以这么写

@GetMapping("/test")
@PreAuthorize("hasAuthority('ADMIN') AND hasAuthority('USER')") 
public String test() {
    return "hello";
}

3). @PostAuthorize
在方法执行后再进行权限验证

@PostAuthorize("returnObject %2 == 0")
@GetMapping(value = "test")
public int test(){
    int i=new Random().nextInt(10);
    System.out.println(i);
    return i;
}

@PostAuthorize 在方法调用完之后进行权限、数据结果检查

4). @PreFilter
@PreFilter 可以对集合类型的参数进行过滤。使用@PreFilter时,Spring Security将移除使对应表达式的结果为false的元素。

@PreFilter(value = "filterObject ==1")
@GetMapping(value = "test")
public List test(List list){
    return list;
}

5). @PostFilter
@PostFilter可以对集合类型的返回值进行过滤。使用@PostFilter时,Spring Security将移除使对应表达式的结果为false的元素。

@PostFilter(value = "filterObject==1")
@GetMapping(value = "test")
public List test(){
    List list = new ArrayList<>();
    list.add(1);
    list.add(2);
    list.add(3);
    list.add(4);
    return list;
}

常用SpringSecurity的标签属性介绍

sec:authorize=”isAuthenticated()”
判断用户是否已经登陆认证,引号内的参数必须是isAuthenticated()。

sec:authentication=“name”
获得当前用户的用户名,引号内的参数必须是name。

sec:authorize=“hasAuthority(‘role’)”
判断当前用户是否拥有指定的权限。引号内的参数为权限的名称。

sec:authentication=”principal.authorities”
获得当前用户的全部角色,引号内的参数必须是principal.authorities。

HttpSecurity 常用方法及说明

方法 说明
openidLogin() 用于基于 OpenId 的验证
headers() 将安全标头添加到响应
cors() 配置跨域资源共享( CORS)
sessionManagement() 允许配置会话管理
portMapper() 允许配置一个PortMapper(HttpSecurity#(getSharedObject(class))),其他提供SecurityConfigurer的对象使用 PortMapper 从 HTTP 重定向到 HTTPS 或者从 HTTPS 重定向到 HTTP。默认情况下,Spring Security使用一个PortMapperImpl映射 HTTP 端口8080到 HTTPS 端口8443,HTTP 端口80到 HTTPS 端口443
jee() 配置基于容器的预认证。 在这种情况下,认证由Servlet容器管理
x509() 配置基于x509的认证
rememberMe 允许配置“记住我”的验证
authorizeRequests() 允许基于使用HttpServletRequest限制访问
requestCache() 允许配置请求缓存
exceptionHandling() 允许配置错误处理
securityContext() 在HttpServletRequests之间的SecurityContextHolder上设置SecurityContext的管理。 当使用WebSecurityConfigurerAdapter时,这将自动应用
servletApi() 将HttpServletRequest方法与在其上找到的值集成到SecurityContext中。 当使用WebSecurityConfigurerAdapter时,这将自动应用
csrf() 添加 CSRF 支持,使用WebSecurityConfigurerAdapter时,默认启用
logout() 添加退出登录支持。当使用WebSecurityConfigurerAdapter时,这将自动应用。默认情况是,访问URL”/ logout”,使HTTP Session无效来清除用户,清除已配置的任何#rememberMe()身份验证,清除SecurityContextHolder,然后重定向到”/login?success”
anonymous() 允许配置匿名用户的表示方法。 当与WebSecurityConfigurerAdapter结合使用时,这将自动应用。 默认情况下,匿名用户将使用org.springframework.security.authentication.AnonymousAuthenticationToken表示,并包含角色 “ROLE_ANONYMOUS”
formLogin() 指定支持基于表单的身份验证。如果未指定FormLoginConfigurer#loginPage(String),则将生成默认登录页面
oauth2Login() 根据外部OAuth 2.0或OpenID Connect 1.0提供程序配置身份验证
requiresChannel() 配置通道安全。为了使该配置有用,必须提供至少一个到所需信道的映射
httpBasic() 配置 Http Basic 验证
addFilterAt() 在指定的Filter类的位置添加过滤器

Springboot 整合 Spring Security 完成。

如果觉得我的文章对您有用,请随意赞赏。您的支持将鼓励我继续创作!