SpringSecurity通用配置

发布于 2022-08-19  1.92k 次阅读


1. 自定义登录页面

    SpringSecurity默认会自动生成一个登录页面(/login),并使用默认URL处理登录的提交内容,登录后跳转到默认URL等。尽管SpringSecurity提供默认登录页面但在真实开发环境中不会使用默认页面,需要自己指定页面

1.1 认证页面准备

认证页面使用Thmeleaf模板

① thymeleaf的maven依赖:

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

② 结构:

③ login.jsp:

<!DOCTYPE html>
<html lang="en" class="no-js">
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Title</title>
</head>
<body>
<form action="/security-springboot/login" method="post">
    用户名:<input type="text" name="username">

    密&nbsp;&nbsp;码:
    <input type="password" name="password">
    <input type="submit" value="登录">
</form>
</body>
</html>

1.2 配置认证页面

① 在实现了WebMvcConfigurer接口的配置文件中配置认证页面地址:

@Configuration
public class WebConfig implements WebMvcConfigurer {

    //默认URL根据跳转到/login,此url为spring security提供
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        //输入项目地址自动重定向到登录页面
        registry.addViewController("/").setViewName("redirect:/loginview");
        //URL绑定登录页面
        registry.addViewController("/loginview").setViewName("/login");
        
    }
}

② 在WebSecurityConfig中配置表单登录信息

@Configuration
public class WebSecurityContext extends WebSecurityConfigurerAdapter {

    //3,安全拦截机制
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .formLogin()//允许表单登录
                    .loginPage("/loginview")//指定自定义登录页,security以重定向方式跳转到/login-view
                    .loginProcessingUrl("/login")//指定登录处理URL,也就是用户名、密码表单提交的目的路径
                    .successForwardUrl("/login-success")//自定义登录成功的跳转页面地址
                    .permitAll();//允许任何用户访问表单登录页
}
}
  • formLogin():允许表单登录
  • loginPage():指定自定义登录页,security以重定向方式跳转到指定页面
  • loginProcessingUrl():指定登录处理URL,也就是用户名、密码表单提交的目的路径
  • successForwardUrl():自定义登录成功的跳转页面地址
  • permitAll():允许任何用户访问表单登录页

测试:

输入账号和密码,点击登录,报错:

1.3 解决错误

问题出现的原因:SpringSecurity为防止CSRF(Cross-site request forgerf跨站请求伪造)的发生,限制了除get以外的大多数方法

1)解决方案1:屏蔽CSRF控制,即SpringSecurity不再限制CSRF

配置WebSecurityConfig

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.csrf().disable();//屏蔽CSRF控制
}

2)解决方案2:再页面请求时添加一个Token,SpringSecurity会验证token,如果token合法则可以继续请求

<form action="/security-springboot/login" method="post">
    <input type="hidden" name="${_csrf.parameterName} value="${_csrf.token}">
</form>

2. 连接数据库认证

 在案例中都是将用户信息使用硬编码的方式存储在内存中,实际项目中用户信息存储在数据库中,这次从数据库读取用户信息进行认证

2.1 创建数据表

表的SQL:

CREATE TABLE `security_user`(
`id` bigint(20) NOT NULL COMMENT '用户id',
`username` VARCHAR(64) NOT NULL,
`password` VARCHAR(64) NOT NULL,
`fullname` VARCHAR(255) NOT NULL,
`mobile` VARCHAR(11) DEFAULT NULL COMMENT "手机号",
PRIMARY KEY (`id`) USING BTREE
)

2.2 数据库模块

① 导入依赖

<dependency>
       <groupId>mysql</groupId>
       <artifactId>mysql-connector-java</artifactId>
       <version>8.0.21</version>
   </dependency>

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

② application.properties配置

spring.datasource.driver-class-name= com.mysql.jdbc.Driver
spring.datasource.url= jdbc:mysql://localhost:3306/springcloud_test?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
spring.datasource.username= root
spring.datasource.password= 123

③ 定义pojo

@Data
public class SecurityUserDao {
    private int id;
    private String username;
    private String password;
    private String fullname;
    private String mobile;
}

④ 定义Dao

@Repository
public class userDao {

    @Autowired
    JdbcTemplate jdbcTemplate;

    //根据用户名查询用户信息
    public SecurityUserDao getUserByUserName(String username){

        String SQL = "SELECT id,username,password,fullname,mobile FROM security_user WHERE username=?";

        List<SecurityUserDao> query = jdbcTemplate.query(SQL, new Object[]{username}, new BeanPropertyRowMapper<SecurityUserDao>(SecurityUserDao.class));

        if(query!=null && query.size()==1){

            return query.get(0);
        }

        return null;
    }}

2.3 权限模块

① SpringSecurity的配置

@Configuration
public class WebSecurityContext extends WebSecurityConfigurerAdapter {

    //2,密码编码器
    @Bean
    public PasswordEncoder passwordEncoder(){
        //不需要对密码进行编码
        return NoOpPasswordEncoder.getInstance();
    }

    //3,安全拦截机制
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/wql/**").authenticated()//所有/wql/**的请求必须认证通过
                .anyRequest().permitAll()//除了/wql/))其他请求可以访问
                .and()
                .formLogin()//允许表单登录
                    .loginPage("/loginview")
                    .loginProcessingUrl("/login")
                    .successForwardUrl("/login-success")//自定义登录成功的页面地址
                    .failureForwardUrl("/login-failure")
                    .permitAll();
        http.csrf().disable();
    }
}
② 自定义SpringSecurity的用户信息模块:
@Service
public class SpringDataUserDetailsService implements UserDetailsService {

    @Autowired
    userDao userDao;

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        //1.将来连接数据库根据账号查询用户信息
        SecurityUserDao userByUserName = userDao.getUserByUserName(s);


        if(userByUserName==null){
            return null;
        }
        //2.登录账号
        UserDetails build = User.withUsername(userByUserName.getUsername()).password(userByUserName.getPassword()).authorities("a1").build();
        return build;
    }
}

3. 连接数据库授权

3.1 数据库准备

① 用户表

CREATE TABLE `security_user`(
`id` bigint(20) NOT NULL COMMENT '用户id',
`username` VARCHAR(64) NOT NULL,
`password` VARCHAR(64) NOT NULL,
`fullname` VARCHAR(255) NOT NULL,
`mobile` VARCHAR(11) DEFAULT NULL COMMENT "手机号",
PRIMARY KEY (`id`) USING BTREE
)

② 角色表

CREATE TABLE `t_role`(
    `id` VARCHAR(32) NOT NULL,
    `role_name` VARCHAR(255) DEFAULT NULL,
    `description` VARCHAR(255) DEFAULT NULL,
    `create_time` DATETIME DEFAULT NULL,
    `update_time` DATETIME DEFAULT NULL,
    `status` char(1) NOT NULL,
    PRIMARY KEY(`id`)
)ENGINE=INNODB DEFAULT CHARSET=utf8

③ 用户角色关系表

CREATE TABLE `t_user_role`(
    `user_id` VARCHAR(32) NOT NULL,
    `role_id` VARCHAR(32) NOT NULL,
    `create_time` datetime DEFAULT NULL,
    `create` VARCHAR(255) DEFAULT NULL,
    PRIMARY KEY(`user_id`,`role_id`)
)ENGINE=InnoDB DEFAULT CHARSET=utf8;

④ 权限表

CREATE TABLE `t_permission`(
    `id` VARCHAR(32) NOT NULL,
    `code` VARCHAR(32) NOT NULL,
    `description` VARCHAR(64) DEFAULT NULL,
    `url` VARCHAR(128) DEFAULT NULL,
    PRIMARY KEY(`id`)
)ENGINE=InnoDB DEFAULT CHARSET=utf8;

⑤ 角色权限关系表

CREATE TABLE `t_role_permission`(
    role_id VARCHAR(32) NOT NULL,
    permission_id VARCHAR(32) NOT NULL,
    PRIMARY KEY(role_id,permission_id)
    )ENGINE=InnoDB DEFAULT CHARSET=utf8;

查询SQL:通过userid查权限

SELECT * FROM t_permission WHERE id IN(
SELECT role_id FROM t_role_permission WHERE role_id IN(
    SELECT role_id FROM t_user_role WHERE user_id =?
))

3.2 授权代码

① POJO

@Data
public class PessmissionPojo {//权限表
    String id;
    String code;
    String description;
    String url;
}

② Dao

@Repository
public class userDao {

    @Autowired
    JdbcTemplate jdbcTemplate;

    //根据用户ID查询用户权限
    public List<String> findPermissionByUserID(String userid){

        String SQL = "SELECT * FROM t_permission WHERE id IN(\n"+
                "SELECT role_id FROM t_role_permission WHERE role_id IN(\n"+
                "SELECT role_id FROM t_user_role WHERE user_id =?\n"+"))";
        List<PessmissionPojo> query = jdbcTemplate.query(SQL, new Object[]{userid}, new BeanPropertyRowMapper<PessmissionPojo>(PessmissionPojo.class));

        ArrayList<String> permission = new ArrayList<>();
        query.forEach((x)->permission.add(x.getCode());

        return permission;
    }
}

③ 自定义安全拦截

@Service
public class SpringDataUserDetailsService implements UserDetailsService {

    @Autowired
    userDao userDao;

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        //1.将来连接数据库根据账号查询用户信息
        SecurityUserPojo userByUserName = userDao.getUserByUserName(s);

        if(userByUserName==null){
            return null;
        }
        //2. 通过用户ID查询权限
        List<String> permissionByUserID = userDao.findPermissionByUserID(new StringBuffer().append(userByUserName.getId()));

        //3. 将permissionByUserID转化成数组
        String[] permissions = new String[permissionByUserID.size()];
        permissionByUserID.toArray(permissions);

        //2.登录账号
        UserDetails build = User.withUsername(userByUserName.getUsername()).password(userByUserName.getPassword()).authorities(permissions).build();
        return build;
    }
}

④ Security配置

@Configuration
public class WebSecurityContext extends WebSecurityConfigurerAdapter {

    //2,密码编码器
    @Bean
    public PasswordEncoder passwordEncoder(){
        //不需要对密码进行编码
        return NoOpPasswordEncoder.getInstance();
    }

    //3,安全拦截机制
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/wql/a1").hasAuthority("p1")//所有/wql/**的请求必须认证通过
                .anyRequest().permitAll()//除了/wql/))其他请求可以访问
                .and()
                .formLogin()//允许表单登录
                    .loginPage("/loginview")
                    .loginProcessingUrl("/login")
                    .successForwardUrl("/login-success")//自定义登录成功的页面地址
                    .failureForwardUrl("/login-failure")
                    .permitAll();
        http.csrf().disable();
    }
}

4. 自定义授权方式

Security授权包括两种:

  • web授权:通过url拦截进行授权
  • 方法授权:通过方法拦截进行授权

    底层它们都会调用accessDecisionManager进行授权决策,若为web授权则拦截器为FilterSecurityInterception,若为方法授权则拦截器为MethodSecurityInterceptor,如果同时通过web授权和方法授权则先执行web授权,在执行方法授权,最后决策通过允许访问资源

类关系如下:

4.1 web授权

Web授权在WebSecurityContext配置文件中的安全拦截方法(configure)中配置

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .antMatchers("/wql/a1").hasAuthority("p1")//所有/wql/**的请求必须认证通过
            .anyRequest().permitAll()//除了/wql/))其他请求可以访问
}
  • anyRequest():表示任意请求
  • authenticated():保护URL,需要用户登录
  • permitAll():指定URL无需保护,一般应用与静态资源文件
  • hasRole(String role):限制单个角色访问,角色将被增加"ROLE_"所以"ADMIN"将和"ROLE_ADMIN"进行比较
  • hasAuthority(String authority):限制单个权限访问
  • hasAnyRole(String……roles):允许多个角色访问
  • hasAnyAuthority(String……authorities):允许多个权限访问
  • access(String attribute):该方法使用SpEL表达式,可以创建复杂的限制
  • haslpAddress(String ipadressExpression):限制IP地址或子网

注:权限的规则的顺序也是非常重要的,更具体的规则应该先写,因为前的规则会覆盖后面的规则,所以在编写权限时需要由小及大

例:权限无效

http.authorizeRequests()
           .anyRequest().permitAll()
           .antMatchers("/wql/a1").hasAuthority("p1")

后的"/wql/a1"访问权限p1会被覆盖掉,应该anyRequest().permitAll()允许所有请求

web授权尽量使用基于资源的授权,减少基于角色的授权

4.2 方法授权

从SpringSecurity2.0开始,它支持服务层方法的安全性支持,方法授权是在Controller方法层面进行授权,通过注解的方式

方法授权的三类注解:

  1. @PreAuthorize
  2. @PostAuthorize
  3. @Secured

4.2.1 @Secured

    需要在任意@Configuration实例上使用@EnableGlobalMethodSecurity注释来启用基于注解的安全性,以下内容启用SpringSecurity的@Secured注释

@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true)
public class WebSecurityContext extends WebSecurityConfigurerAdapter {
………………
}

例:

@GetMapping("/helloUser") 
@Secured({"ROLE_normal","ROLE_admin"}) 
public String helloUser() {     
    return "hello,user"; 
    }

拥有normal或者admin角色的用户都可以方法helloUser()方法。另外需要注意的是这里匹配的字符串需要添加前缀“ROLE_“

假如要设置同时拥有admin & noremal的用户才能方法helloUser()方法,这时候@Secured就无能为力了,需要其他两个注解来完成

4.2.2 @PreAuthorize

    Spring的 @PreAuthorize/@PostAuthorize 注解更适合方法级的安全,也支持Spring SpEL表达式语言,提供了基于表达式的访问控制

 当@EnableGlobalMethodSecurity(prePostEnabled=true)的时候,@PreAuthorize可以使用:

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled= true)
public class WebSecurityContext extends WebSecurityConfigurerAdapter {
………………
}

例:拥有normal或者admin角色的用户都可以方法helloUser()方法,此时如果我们要求用户必须同时拥有normal和admin的话,那么可以这么编码

@GetMapping("/helloUser")
@PreAuthorize("hasRole('normal') AND hasRole('admin')")  
public String helloUser() {     
    return "hello,user"; 
    }

4.2.3 @PostAuthorize

  @PostAuthorize 注解使用并不多,在方法执行后再进行权限验证,适合验证带有返回值的权限,Spring EL 提供 返回对象能够在表达式语言中获取返回的对象returnObject

当@EnableGlobalMethodSecurity(prePostEnabled=true)的时候,@PostAuthorize可以使用:

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled= true)
public class WebSecurityContext extends WebSecurityConfigurerAdapter {
………………
}

例:

@GetMapping("/helloUser") 
@PostAuthorize(" returnObject!=null &&  returnObject.username == authentication.name") public User helloUser() {         
    Object pricipal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();         
    User user;         
if("anonymousUser".equals(pricipal)) { 
            user = null;         
   }else {             
     user = (User) pricipal;         
    }         
        return user; 
        }

注: 这三个最常用也就是@PreAuthorize这个注解了,在使用中主要是配合Spring EL表达式。

5. 会话管理 

    用户认证通过后,为了避免用户的每次操作都进行认证可将用户的信息保存在会话中。SpringSecurity提供会话管理,认证通过后将身份信息存入SecurityContextHolder上下文,SecurityContext与当前线程进行绑定,方便获取用户身份

5.1 获取会话信息

编写LoginController,实现/wql/a1和/wql/a2的测试资源,并修改success和failure方法

@RestController
public class logincontroller {

    @RequestMapping(value = "/login-success", produces = "text/plain;charset=UTF-8")
    public String success() {
        String username =getUsername();
        return username+"登录成功";
    }

    @RequestMapping(value = "/login-failure", produces = "text/plain;charset=UTF-8")
    public String failure() {
        String username =getUsername();
        return username+"登录失败";
    }


    //获取用户名
    private String getUsername() {

        //用户名
        String username = null;

        //通过SecurityContextHolder获取认证通过的用户信息Authentication
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

        //通过Authentication的用户身份信息
        Object principal = authentication.getPrincipal();

        if (principal == null) {
            username = "匿名用户";
        }


        //判断principal是否是UserDetails的子类
        if (principal instanceof UserDetails) {
            //强转
            UserDetails userDetails = (UserDetails) principal;
            //获取username
            username = userDetails.getUsername();
        }
        return username;
    }
}

 通过SecurityContextHolder.getContext().getAuthentication()获取Authentication,而Authentication就保存权限信息、细节信息、身份信息等

测试:

5.2 会话控制

除了获取会话信息,SpringSecurity还提供了很多会话控制的参数

机制 描述
always 如果没有session存在就创建一个
ifRequired 如果需要就创建一个Session(默认)登录时
never SpringSecurity将不会创建Session,但是如果应用中其他地方创建了Session,那么SpringSecurity将会使用它
stateless SpringSecurity将绝对不会创建Session,也不使用session

通过以下配置方式对该选项进行配置:

@Configuration
public class WebSecurityContext extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED);
    /*
        SessionCreationPolicy.ALWAYS,
        SessionCreationPolicy.NEVER
        SessionCreationPolicy.IF_REQUIRED
        SessionCreationPolicy.STATELESS
    */
    }
}
  • 默认情况下, Spring Security会为每个登录成功的用户会新建一个Session,就是ifRequired
  • 若选用never,则指示Spring Security对登录成功的用户不创建Session了,但若你的应用程序在某地方新建session ,那么Spring Security会用它的
  • 若使用stateless,则说明Spring Security对登录成功的用户不会创建Session了,你的应用程序也不会允许新建session。并且它会暗示不使用cookie,所以每个请求都需要重新进行身份验证。这种无状态架构适用于REST API及其无状态认证机制(也就是token方式)

5.3 会话超时

可以在Sevlet容器中设置Session的超时时间,如果设置Session有效期为3600s

Spring boot配置文件:

server.servlet.session.timeout=3600s

session超时之后,可以通过Spring Security设置跳转的路径

http.sessionManagement()
        .expiredUrl("")
        .invalidSessionUrl("");
  • expired:指session过期
  • invalidSession:指传入的session无效

5.4 安全会话

使用httpOnly和security标签来保护会话cookie

  • httpOnly:如果为true,那么浏览器脚本将无法访问cookie
  • secure:如果为true,则cookie将仅能通过HTTPS连接发生

application配置文件:

server.servlet.session.cookie.http-only=true
server.servlet.session.cookie.secure=true

6. 自定义退出

SpringSecurity默认实现了logout退出,默认访问/logout就会出现默认退出页

点击"Log Out"退出成功,退出后访问其他URL判断是否成功退出

在开发时自定义这些页面是必须的,所以一般会替代成指定页面

在WebSecurityConfig中配置退出页面

@Configuration
public class WebSecurityContext extends WebSecurityConfigurerAdapter {

        http.logout()//声明退出,使Session无效
                .logoutUrl("/logout")//退出页面URL
                .logoutSuccessUrl("/loginview?login");//退出成功后重定向的URL
    }
}

当退出操作触发时将发生的事件:

  • 使HTTP Session无效
  • 清除SecurityContextHolder
  • 重定向到/loginview?login

在基本配置的基础上进一步配置:

http.logout()
        .logoutUrl("/logout")
        .logoutSuccessUrl("/loginview?login")
        .logoutSuccessHandler()
        .addLogoutHandler()
        .invalidateHttpSession(true);
  1. 提供系统退出支持,使用WebSecurityConfigurerAdapter会自动被应用
  2. 定制的logoutSuccessHandler,用于实现用户退出成功时的处理类,如果指定了这个选项那么logoutSuccessUrl()的设置会被忽略
  3. 添加一个LogoutHandler,用于实现用户退出时的清理工作,默认SecurityContextLogoutHandler会被添加为最后一个LogoutHandler
  4. 指定是否在退出时让HttpSession无效

注:如果让logout在GET请求下生效,必须关闭防止CSPF攻击csrf().disable()。如果开启CSRF,必须使用post方式请求/logout

logoutHandler接口:LogoutHandler的实现类被用于执行必要的清理,因而他们不应该抛出异常

下面是SpringSecurity提供的一些实现:

实现类
作用
PersistentTokenBasedRememberMeServices
基于持久化token的RememberMe功能的相关清理
TokenBasedRememberMeService
基于tokenRememberMe功能的相关清理
CookieClearingLogoutHandler
退出时Cookie的相关清理
CsrfLogoutHandler
负责在退出时移除csrfToken
SecurityContextLogoutHandler
 
退出时SecurityContext的相关清理