权限控制与安全认证 Spring Security

一、Spring Security介绍

 Spring Security是Spring项目组提供的安全服务框架,核心功能包括认证授权。它为系统提供了声明式安全访问控制功能,减少了为系统安全而编写大量重复代码的工作。

认证

认证即系统判断用户的身份是否合法,合法可继续访问,不合法则拒绝访问。常见的用户身份认证方式有:用户名密码登录、二维码登录、手机短信登录、脸部识别认证、指纹认证等方式。认证是为了保护系统的隐私数据与资源,用户的身份合法才能访问该系统的资源。

授权

授权即认证通过后,根据用户的权限来控制用户访问资源的过程,拥有资源的访问权限则正常访问,没有权限则拒绝访问。 比如在一些视频网站中,普通用户登录后只有观看免费视频的权限,而VIP用户登录后,网站会给该用户提供观看VIP视频的权限。

认证是为了保证用户身份的合法性,授权则是为了更细粒度的对隐私数据进行划分,控制不同的用户能够访问不同的资源。

举个例子:认证是公司大门识别你作为员工能进入公司,而授权则是由于你作为公司会计可以进入财务室,查看账目,处理财务数据。

二、Spring Security认证

2.1 项目搭建

1、准备一个名为mysecurity的Mysql数据库

 2、创建SpringBoot项目,添加依赖(myabtisplus需要在pom中添加依赖)

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.0</version>
        </dependency>

3、编写配置文件

server:
  port: 80


#日志格式
logging:
  pattern:
    console: '%d{HH:mm:ss.SSS} %clr(%-5level) ---  [%-15thread] %cyan(%-50logger{50}):%msg%n'


# 数据源
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql:///mysecurity?serverTimezone=UTC
    username: root
    password: 123456

4、在template文件夹编写项目主页面main.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>主页面</title>
</head>
<body>
<h1>主页面</h1>
</body>
</html>

5、编写访问页面控制器

@Controller
public class PageController {
    @RequestMapping("/{page}")
    public String showPage(@PathVariable String page){
        return page;
    }
}

启动项目,访问项目主页面http://localhost/main,项目会自动跳转到一个登录页面。这代表Spring Security已经开启了认证功能,不登录无法访问所有资源,该页面就是Spring Security自带的登录页面。

我们使用user作为用户名,控制台中的字符串作为密码登录,登录成功后跳转到项目主页面。

在后续的文章中,会介绍在真实开发中,如何对登录页面、登录逻辑等进行自定义配置。

2.2 内存认证

 在实际开发中,用户数量不会只有一个,且密码是自己设置的。所以我们需要自定义配置用户信息。首先我们在内存中创建两个用户,Spring Security会将登录页传来的用户名密码和内存中用户名密码做匹配认证。

package com.zj.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.jaas.memory.InMemoryConfiguration;
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.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

//配置类
@Configuration
public class SecurityConfig {
    //内存数据认证逻辑
    @Bean
    public UserDetailsService userDetailsService(){
        //1.使用内存数据进行认证
        InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
        //2.创建两个用户
        UserDetails user1 = User.withUsername("zj").password("123").authorities("admin").build();
        UserDetails user2 = User.withUsername("zs").password("456").authorities("admin").build();
        //3.将两个用户保存到内存中
        inMemoryUserDetailsManager.createUser(user1);
        inMemoryUserDetailsManager.createUser(user2);

        return inMemoryUserDetailsManager;
    }

    //密码编码器,将密码加密保存
    @Bean
    public PasswordEncoder passwordEncoder(){
        //设置以明文保存,不加密。
        return NoOpPasswordEncoder.getInstance();
    }
}

此时进行认证测试,我们可以将登录页传来的用户名密码和内存中用户名密码做匹配认证。

2.3 UserDetailsService

在实际项目中,认证逻辑是需要自定义控制的。将UserDetailsService接口的实现类放入Spring容器即可自定义认证逻辑。InMemoryUserDetailsManager就是UserDetailsService接口的一个实现类,它将登录页传来的用户名密码和内存中用户名密码做匹配认证。当然我们也可以自定义UserDetailsService接口的实现类。

public interface UserDetailsService {
  UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

UserDetailsService的实现类必须重写loadUserByUsername方法,该方法定义了具体的认证逻辑,参数username是前端传来的用户名,我们需要根据传来的用户名查询到该用户(一般是从数据库查询),并将查询到的用户封装成一个UserDetails对象,该对象是Spring Security提供的用户对象,包含用户名、密码、权限。Spring Security会根据UserDetails对象中的密码和客户端提供密码进行比较。相同则认证通过,不相同则认证失败。

2.4 数据库认证

1、创建数据库表

2、编写用户实体类

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Users extends Model<Users> {
  private Integer id;
  private String username;
  private String password;
  private String phone;
}

3、编写mapper接口

public interface UsersMapper extends BaseMapper<Users> {
}

4、在 SpringBoot启动类中添加 @MapperScan 注解,扫描Mapper

@SpringBootApplication
@MapperScan("com.zj.mapper")
public class SecurityApplication {

    public static void main(String[] args) {
        SpringApplication.run(SecurityApplication.class, args);
    }

}

5、创建UserDetailsService的实现类,编写自定义认证逻辑

package com.zj.service;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.zj.pojo.Users;
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.stereotype.Service;

@Service
public class MyUserDetailsService implements UserDetailsService {

  //自定义认正逻辑
  @Override
  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    //1.构造查询条件
    QueryWrapper<Users> query = new QueryWrapper<Users>();
    query.eq("username", username);
    //2.查询用户
    Users users = new Users();
    Users users1 = users.selectOne(query);
    if (users1 == null) {
      return null;
    }
    //3.将查询结果封装为UserDetails对象返回
    UserDetails userDetails = User.withUsername(users1.getUsername())
            .password(users1.getPassword()).authorities("admin").build();
    return userDetails;
  }
}

6、登录访问即可

2.5 PasswordEncoder

在实际开发中,为了数据安全性,在数据库中存放密码时不会存放原密码,而是会存放加密后的密码。而用户传入的参数是明文密码。此时必须使用密码解析器才能将加密密码与明文密码做比对。Spring Security中的密码解析器是PasswordEncoder

Spring Security要求容器中必须有PasswordEncoder实例,之前使用的NoOpPasswordEncoderPasswordEncoder的实现类,意思是不解析密码,使用明文密码。

Spring Security官方推荐的密码解析器是BCryptPasswordEncoder。接下来我们学习BCryptPasswordEncoder的使用。

package com.zj.security;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@SpringBootTest
public class TestPasswordEncoder {

    @Test
    public void testBCryptPasswordEncoder(){
        //创建解析器
        BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
        //密码加密
        String pwd = bCryptPasswordEncoder.encode("12345");
        System.out.println("加密后的密码:"+pwd);
        //密码校验:明文和密文校验
        boolean matches = bCryptPasswordEncoder.matches("12345", pwd);
        System.out.println("是否校验成功:"+matches);
    }
}
加密后的密码:$2a$10$CQ0RghViS0.5I6kEpnNGeekDD44Wmt.sh876GCB6iCjjMyg39hNKe
是否校验成功:true

需要注意的是,每次加密的密文都是不一样的,但是只要是该明文加密形成的密文。都能和明文验证成功。

2.6 自定义登录页面

虽然Spring Security给我们提供了登录页面,但在实际项目中,更多的是使用自己的登录页面。Spring Security也支持用户自定义登录页面。用法如下:

 1、在template目录下编写登陆页面、登录失败页面、主页面;将css文件放在static目录下面。

2、在Spring Security配置类自定义登录页面,注意该配置类要继承 WebSecurityConfigurerAdapter类。

package com.zj.config;

import org.springframework.cglib.proxy.NoOp;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //自定义表单登录
       http.formLogin()
               .loginPage("/login.html")//自定义登录页面
               .usernameParameter("username")//表单中的用户名
               .passwordParameter("password")//表单中的密码
               .loginProcessingUrl("/login")//表单提交到security的路径。
               .successForwardUrl("/main") //认证成功后跳转路径
               .failureForwardUrl("/fail");//认证失败后跳转路径

        //需要认证的资源
        http.authorizeHttpRequests()
                .antMatchers("/login.html").permitAll()//登录页不需要认证
                .anyRequest().authenticated(); //其他请求都需要认证

        //关闭CSRF防护
        http.csrf().disable();
    }


    @Override
    public void configure(WebSecurity web) throws Exception {
        //静态资源放行
        web.ignoring().antMatchers("/css/**");
    }

    
    //密码解析方式:不解析
    @Bean
    public PasswordEncoder passwordEncoder(){
        return NoOpPasswordEncoder.getInstance();
    }
}

2.7 CSRF防护

CSRF:跨站请求伪造,通过伪造用户请求访问受信任的站点从而进行非法请求访问,是一种攻击手段。 Spring Security为了防止CSRF攻击,默认开启了CSRF防护,这限制了除了GET请

求以外的大多数方法。我们要想正常使用Spring Security需要突破CSRF防护。

解决方法一:关闭CSRF防护.

http.csrf().disable();

解决方法二:突破CSRF防护.

CSRF为了保证不是其他第三方网站访问,要求访问时携带参数名为_csrf值为令牌,令牌在服务端产生,如果携带的令牌和服务端的令牌匹配成功,则正常访问。

<form class="form" action="/login" method="post">
 <!-- 在表单中添加令牌隐藏域 -->
 <input type="hidden" th:value="${_csrf.token}" name="_csrf" th:if="${_csrf}"/>
 <input type="text" placeholder="用户名" name="username">
 <input type="password" placeholder="密码" name="password">
 <button type="submit">登录</button>
</form>

2.8 会话管理

用户认证通过后,有时我们需要获取用户信息,比如在网站顶部显示:欢迎您,XXX。Spring Security将用户信息保存在会话中,并提供会话管理,我们可以从SecurityContext对象中获取用户信息,SecurityContext对象与当前线程进行绑定。

package com.zj.controller;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MyController {
    //获取当前登录的用户名
    @RequestMapping("/name")
    public String getName(){
        //1.获取会话对象
        SecurityContext context = SecurityContextHolder.getContext();
        //2.获取认证对象
        Authentication authentication = context.getAuthentication();
        //3.获取用户信息
        UserDetails userDetails = (UserDetails) authentication.getPrincipal();
        return userDetails.getUsername();
    }
}

 2.9 认证成功后的处理方式

登录成功后,如果除了跳转页面还需要执行一些自定义代码时,如:统计访问量,推送消息等操作时,可以自定义登录成功处理器。

1、自定义登录成功处理器

public class MyLoginSuccessHandler implements AuthenticationSuccessHandler {
  @Override
  public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
    // 拿到登录用户的信息
    UserDetails userDetails = (UserDetails)authentication.getPrincipal();
    System.out.println("用户名:"+userDetails.getUsername());
    System.out.println("一些操作...");


    // 重定向到主页
    response.sendRedirect("/main");
   }
}

2、在security配置类中修改配置

http.formLogin() // 使用表单登录
   .loginPage("/login.html") // 自定义登录页面
   .usernameParameter("username") // 表单中的用户名项
   .passwordParameter("password") // 表单中的密码项
   .loginProcessingUrl("/login") // 登录路径,表单向该路径提交,提交后自动执行UserDetailsService的方法
  //         .successForwardUrl("/main")  //登录成功后跳转的路径
   .successHandler(new MyLoginSuccessHandler()) //登录成功处理器
   .failureForwardUrl("/fail"); //登录失败后跳转的路径

这样只有在控制台输出完成后才会跳转到主页面。

2.10 认证失败后的处理方式

登录失败后,如果除了跳转页面还需要执行一些自定义代码时,如:统计失败次数,记录日志等,可以自定义登录失败处理器。 

1、自定义登录失败处理器

public class MyLoginFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        System.out.println("记录失败日志");
        response.sendRedirect("/fail");
    }
}

2、配置登录失败处理器

http.formLogin() // 使用表单登录
   .loginPage("/login.html") // 自定义登录页面
   .usernameParameter("username") // 表单中的用户名项
   .passwordParameter("password") // 表单中的密码项
   .loginProcessingUrl("/login") // 登录路径,表单向该路径提交,提交后自动执行UserDetailsService的方法
  //         .successForwardUrl("/main")  //登录成功后跳转的路径
   .successHandler(new MyLoginSuccessHandler()) //登录成功处理器
  //         .failureForwardUrl("/fail") //登录失败后跳转的路径
   .failureHandler(new MyLoginFailureHandler()); //登录失败处理器


// 需要认证的资源
http.authorizeRequests()
   .antMatchers("/login.html").permitAll() // 登录页不需要认证
   .antMatchers("/fail").permitAll() // 失败页不需要认证
   .anyRequest().authenticated(); //其余所有请求都需要认证

2.11 退出登录

在系统中一般都有退出登录的操作。退出登录后,Spring Security进行了以下操作:

  • 清除认证状态
  • 销毁HttpSession对象
  • 跳转到登录页面

在Spring Security中,退出登录的写法如下:

 1、配置退出登录的路径和退出后跳转的路径

       // 退出登录配置
        http.logout()
                .logoutUrl("/logout") // 退出登录路径
                .logoutSuccessUrl("/login.html") // 退出登录后跳转的路径
                .clearAuthentication(true) //清除认证状态,默认为true
                .invalidateHttpSession(true); // 销毁HttpSession对象,默认为true

2、在网页中添加退出登录超链接

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>主页面</title>
</head>
<body>
<h1>主页面</h1>
<a href="/logout">退出登录</a>
</body>
</html>

2.12 退出成功处理器

 我们也可以自定义退出成功处理器,在退出后清理一些数据,写法如下:

1、自定义退出成功处理器

public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
  @Override
  public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException, IOException {
    System.out.println("清除一些数据...");
    response.sendRedirect("/login.html");
   }
}

2、配置退出成功处理器

// 退出登录配置
http.logout()
   .logoutUrl("/logout") // 退出登录路径
  //         .logoutSuccessUrl("/login.html") // 退出登录后跳转的路径
   .clearAuthentication(true) //清除认证状态,默认为true
   .invalidateHttpSession(true) // 销毁HttpSession对象,默认为 true
   .logoutSuccessHandler(new MyLogoutSuccessHandler()); //自定义退出成功处理器

2.13  Remember Me

 Spring Security中Remember Me为“记住我”功能,即下次访问系统时无需重新登录。当使用“记住我”功能登录后,Spring Security会生成一个令牌(一串字符串),令牌一方面保存到数据库中,另一方面生成一个叫remember-me的Cookie保存到客户端。之后客户端访问项目时自动携带令牌,不登录即可完成认证。

1、编写“记住我”配置类

package com.zj.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;

import javax.annotation.Resource;
import javax.sql.DataSource;

@Configuration
public class RememberMeConfig {

    @Resource
    private DataSource dataSource;

    //连接数据库的工具类
    @Bean
    public PersistentTokenRepository getPersistentTokenRepository() {
        //1.为spring security 自带的令牌控制器设置数据源
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        jdbcTokenRepository.setDataSource(dataSource);

        //2.自动创建令牌表(第一次启动时需要,第二次启动时注释就行)
        jdbcTokenRepository.setCreateTableOnStartup(true);

        return jdbcTokenRepository;
    }
}

2、修改Security配置类

    @Resource
    private UserDetailsService userDetailsService;

    @Resource
    private PersistentTokenRepository persistentTokenRepository;



        //记住我配置
        http.rememberMe()
                .userDetailsService(userDetailsService) //登录逻辑
                .tokenRepository(persistentTokenRepository)  //令牌工具类
                .tokenValiditySeconds(30);// 保存30秒

3、在登录页面添加“记住我”复选框

            <form class="form" action="/login" method="post">
                <input type="text" placeholder="用户名" name="username">
                <input type="password" placeholder="密码" name="password">
                <!--name必须是 remember-me-->
                <input type="checkbox" name="remember-me" value="true"/>记住我</br>
                <button type="submit">登录</button>
            </form>

 4、启动项目,数据库创建出token表。

 5、将记住我配置中的创建表的配置注释掉。

        //2.自动创建令牌表(第一次启动时需要,第二次启动时注释就行)
//        jdbcTokenRepository.setCreateTableOnStartup(true);

三、Spring Security授权

3.1 RBAC

 授权即认证通过后,系统给用户赋予一定的权限,用户只能根据权限访问系统中的某些资源。RBAC是业界普遍采用的授权方式,它有两种解释:

Role-Based Access Control

基于角色的访问控制,即按角色进行授权。比如在企业管理系统中,主体角色为总经理可以查询企业运营报表。逻辑为:

if(主体.hasRole("总经理角色")){ 
    查询运营报表
}

如果查询企业运营报表的角色变化为总经理和股东,此时就需要修改判断逻辑代码:

if(主体.hasRole("总经理角色") || 主体.hasRole("股东角色")){ 
    查询运营报表
}

此时我们可以发现,当需要修改角色的权限时就需要修改授权的相关代码,系统可扩展性差。

Resource-Based Access Control

基于资源的访问控制,即按资源(或权限)进行授权。比如在企业管理系统中,用户必须 具有查询报表权限才可以查询企业运营报表。逻辑为:

if(主体.hasPermission("查询报表权限")){ 
    查询运营报表
}

这样在系统设计时就已经定义好查询报表的权限标识,即使查询报表所需要的角色变化为总经理和股东也不需要修改授权代码,系统可扩展性强。该授权方式更加常用。

 

3.2 权限表设计

用户和权限的关系为多对多,即用户拥有多个权限,权限也属于多个用户,所以建表方式如下:

 

这种方式需要指定用户有哪些权限,如:张三有查询工资的权限,即在用户权限中间表中添加一条数据,分别记录张三和查询工资权限ID。但在系统中权限数量可能非常庞大,如果一条一条添加维护数据较为繁琐。所以我们通常的做法是再加一张角色表:

 

用户角色,角色权限都是多对多关系,即一个用户拥有多个角色,一个角色属于多个用户;一个角色拥有多个权限,一个权限属于多个角色。这种方式需要指定用户有哪些角色,而角色又有哪些权限。

如:张三拥有总经理的角色,而总经理拥有查询工资、查询报表的权限,这样张三就拥有了查询工资、查询报表的权限。这样管理用户时只需管理少量角色,而管理角色时也只需要管理少量权限即可。

接下来我们创建五张表:

 3.3 编写查询权限方法

在认证后进行授权需要根据用户id查询到用户的权限,写法如下:

1、编写用户、角色、权限实体类

// 不要命名为User,避免和Spring Security提供的User混淆
@Data
public class Users {
  private Integer uid;
  private String username;
  private String password;
  private String phone;
}


// 角色
@Data
public class Role {
  private String rid;
  private String roleName;
  private String roleDesc;
}


// 权限
@Data
public class Permission {
  private String pid;
  private String permissionName;
  private String url;
}

2、编写UsersMapper接口

// 根据用户名查询权限
List<Permission> findPermissionByUsername(String username);

3、编写UsersMapper.xml文件

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zj.mapper.UsersMapper">
    <select id="findPermissionByUsername" parameterType="string" resultType="com.zj.pojo.Permission">
           select DISTINCT permission.permissionName,permission.pid,permission.url
           from users LEFT JOIN users_role on users.uid = users_role.uid
                      LEFT JOIN role on users_role.rid = role.rid
                      LEFT JOIN role_permission on role.rid =role_permission.rid
                      LEFT JOIN permission on role_permission.pid = permission.pid
           where users.username = #{username}
    </select>
</mapper>

4、测试方法

package com.zj.security;

import com.zj.mapper.UsersMapper;
import com.zj.pojo.Permission;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import javax.annotation.Resource;
import java.util.List;

@SpringBootTest
public class UsersMapperTest {

    @Resource
    private UsersMapper usersMapper;

    @Test
    public void testFindPermissionByUsername(){
        List<Permission> permissions = usersMapper.findPermissionByUsername("张三");
        for (Permission permission : permissions) {
            System.out.println(permission);
        }
    }

}
Permission(pid=1, permissionName=查询报表, url=/reportform/find)
Permission(pid=3, permissionName=查询税务, url=/tax/find)
Permission(pid=2, permissionName=查询工资, url=/salary/find)

5、修改MyUserDetailsService中对用户权限的处理。

package com.zj.service;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.zj.mapper.UsersMapper;
import com.zj.pojo.Permission;
import com.zj.pojo.Users;
import org.springframework.security.core.GrantedAuthority;
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.stereotype.Service;

import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;

@Service
public class MyUserDetailsService implements UserDetailsService {

  @Resource
  private UsersMapper usersMapper;

  //自定义认正逻辑
  @Override
  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    //1.构造查询条件
    QueryWrapper<Users> query = new QueryWrapper<Users>();
    query.eq("username", username);
    //2.查询用户
    Users users = new Users();
    Users users1 = users.selectOne(query);
    if (users1 == null) {
      return null;
    }
    //3.查询用户权限
    List<Permission> permissions = usersMapper.findPermissionByUsername(users1.getUsername());
    //4.将自定义权限集合转为security自带的权限集合
    List<GrantedAuthority> authorities = new ArrayList<>();
    for (Permission permission : permissions) {
      /*SimpleGrantedAuthority为GrantedAuthority接口的实现类*/
       authorities.add(new SimpleGrantedAuthority(permission.getUrl()));
    }

    //5.将查询结果封装为UserDetails对象返回
    UserDetails userDetails = User.withUsername(users1.getUsername())
            .password(users1.getPassword()).authorities(authorities).build();
    return userDetails;
  }
}

3.4 配置类设置访问控制

在给用户授权后,我们就可以给系统中的资源设置访问控制,即拥有什么权限才能访问什么资源。

 1、编写控制器类,添加控制器方法资源

@RestController
public class MyController {
  @GetMapping("/reportform/find")
  public String findReportForm() {
    return "查询报表";
   }


  @GetMapping("/salary/find")
  public String findSalary() {
    return "查询工资";
   }


  @GetMapping("/staff/find")
  public String findStaff() {
    return "查询员工";
   }
}

2、修改Security配置类

        //需要认证的资源
        http.authorizeHttpRequests()
                .antMatchers("/login.html").permitAll()//登录页不需要认证
                .antMatchers("/reportform/find").hasAnyAuthority("/reportform/find")
                .antMatchers("/salary/find").hasAnyAuthority("/salary/find")
                .antMatchers("/staff/find").hasAnyAuthority("/staff/find")
                .anyRequest().authenticated(); //其他请求都需要认证

3、登录并测试

搞定!!! 

3.5 自定义访问控制逻辑

如果资源数量很多,一条条配置需要的权限效率较低。我们可以自定义访问控制逻辑,即访问资源时判断用户是否具有名为该资源URL的权限。

1、自定义访问逻辑

package com.zj.service;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.util.Collection;

@Service
public class MyAuthorizationService {

    // 自定义访问控制逻辑,返回值为是否可以访问资源
    public boolean hasPermission(HttpServletRequest request, Authentication authentication){
        //1.获取认证的用户
        UserDetails userDetails = (UserDetails) authentication.getPrincipal();
        //2.获取登录用户的权限
        Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();
        //3.获取请求的url
        String url = request.getRequestURI();
        //4.将url封装为权限对象
        SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(url);
        //5.判断用户权限集合中是否包含请求的url权限对象
        return authorities.contains(simpleGrantedAuthority);
    }
}

2、配置类中使用自定义访问逻辑

       // 权限拦截配置
        http.authorizeRequests()
                .antMatchers("/login.html").permitAll() // 表示任何权限都可以访问
                // 任何请求都使用自定义访问控制逻辑
                .anyRequest().access("@myAuthorizationService.hasPermission(request,authentication)");

3.6 @Secured设置访问控制

除了配置类,在SpringSecurity中提供了一些访问控制的注解。这些注解默认都是不可用的,需要开启后使用。

该注解是基于角色的权限控制,要求UserDetails中的权限名必须以ROLE_开头,当然数据库中的权限也是要以ROLE_开头。

 1、在启动类开启注解使用


@SpringBootApplication
@MapperScan("com.zj.mapper")
@EnableGlobalMethodSecurity(securedEnabled=true)
public class SecurityApplication {

    public static void main(String[] args) {
        SpringApplication.run(SecurityApplication.class, args);
    }

}

2、在控制器方法上添加注解

@Secured("ROLE_reportform")
@GetMapping("/reportform/find")
public String findReportForm() {
  return "查询报表";
}

3.7 @PreAuthorize设置访问控制

该注解可以在方法执行前判断用户是否具有权限

1、在启动类开启注解使用

@SpringBootApplication
@MapperScan("com.zj.mapper")
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityApplication {

    public static void main(String[] args) {
        SpringApplication.run(SecurityApplication.class, args);
    }

}

2、在控制器方法上添加注解

@PreAuthorize("hasAnyAuthority('/reportform/find')")
@GetMapping("/reportform/find")
public String findReportForm() {
  return "查询报表";
}

3、取消配置类中对权限的访问配置

//       // 权限拦截配置
//        http.authorizeRequests()
//                .antMatchers("/login.html").permitAll() // 表示任何权限都可以访问
//                // 任何请求都使用自定义访问控制逻辑
//                .anyRequest().access("@myAuthorizationService.hasPermission(request,authentication)");

 

3.8 前端进行访问控制

 SpringSecurity可以在一些视图技术中进行控制显示效果。例如Thymeleaf中,只有登录用户拥有某些权限才会展示一些菜单。

1、在pom中引入Spring Security和Thymeleaf的整合依赖(在创建项目选择起步依赖的时候默认就添加上了)

<!--Spring Security整合Thymeleaf-->
<dependency>
  <groupId>org.thymeleaf.extras</groupId>
  <artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>

2、在Thymeleaf中使用Security标签,控制前端的显示内容

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
   xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<head>
  <meta charset="UTF-8">
  <title>主页面</title>
</head>
<body>
<h1>主页面</h1>
<ul>
  <li sec:authorize="hasAnyAuthority('/reportform/find')"><a href="/reportform/find">查询报表</a></li>
  <li sec:authorize="hasAnyAuthority('/salary/find')"><a href="/salary/find">查询工资</a></li>
  <li sec:authorize="hasAnyAuthority('/staff/find')"><a href="/staff/find">查询员工</a></li>
</ul>
<a href="/logout">退出登录</a>
</body>
</html>

这样面对不同权限的用户,前端可以显示不同的菜单。

3.9 403处理方案

使用Spring Security时经常会看见403(无权限),这样的页面很不友好,我们可以自定义403异常处理方案:

 1、编写权限不足页面noPermission.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>权限不足</title>
</head>
<body>
<h1>您的权限不足,请联系管理员!</h1>
</body>
</html>

2、编写权限不足处理类

public class MyAccessDeniedHandler implements AccessDeniedHandler {
  @Override
  public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
    response.sendRedirect("/noPermission.html");
   }
}

3、在Spring Security配置文件中配置异常处理

//异常处理
http.exceptionHandling().
        accessDeniedHandler(new MyAccessDeniedHandler());

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

张小猿ε٩(๑> ₃ <)۶ з

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

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

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

打赏作者

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

抵扣说明:

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

余额充值