Spring Security对用户权限进行管理

基于角色的访问控制
隐式访问控制:根据用户的角色判断是否具有某项操作
显示访问控制:为用户分配某种权限,根据权限判断是否可以进行某种操作。与具体角色无关,权限控制更加灵活

认证( authentication ):建立主体( principal) ,确认用户角色。"主体"不仅指用户,还可以是其他执行操作的系统或设备。
授权(authorization ):或称为"访问控制(access-control), 是指决定是否允许主体在应用程序中执行某种操作

Spring Security是Spring中常用的安全服务框架,它包含如下模块:

  • 核心,spring-security-core.jar
  • 配置,spring-security-config.jar
  • Remoting,spring-security-remoting.jar
  • Web安全,spring-security-web.jar
  • LDAP认证,spring-security-ldap.jar
  • ACL访问控制,spring-security-acl.jar
  • CAS单点登录,spring-security-cas.jar
  • OpenId,spring-security-openid.jar
  • 测试,spring-security-test.jar

通过gradle引入Spring Security依赖如下

dependencies {
	implementation('org.springframework.boot:spring-boot-starter-security')
	...
}

基于角色的安全管理

角色权限类Authority

首先创建表示权限的类Authority实现GrantedAuthority接口
所有的Authentication实现类都保存了一个GrantedAuthority列表表示用户所具有的权限,通过AuthenticationManager设置到Authentication对象中的,然后AccessDecisionManager将依据GrantedAuthority来鉴定用户是否具有访问对应资源的权限。
GrantedAuthority是一个接口,其中只定义了一个getAuthority()方法,其返回值为String类型。该方法允许AccessDecisionManager获取一个能够精确代表该权限的字符串。

@Entity
public class Authority implements GrantedAuthority {
    private static final long serialVersionUID = 1L;
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(nullable = false)
    private String authority;

    public Long getId() {        return id;    }
    public void setId(Long id) {        this.id = id;    }

    @Override
    public String getAuthority() {        return authority;    }
    public void setAuthority(String authority) { this.authority = authority;  }
}

用户实体类User

接着需要创建用户角色类User,它实现了Spring Security的UserDetails接口,该接口需要重写getAuthorities()、getUsername()、getPassword()、isAccountNonExpired()、isEnabled()等方法用于获取、设置用户的基本信息。

其中@ManyToMany声明多对多映射,即User中用authorities字段表示用户的多个权限类Authority,并通过@JoinTable连接到对应的数据表user_authority。

getAuthorities()返回权限列表时,需要将列表authorities中的Authority类型权限转换为SimpleGrantedAuthority类型后再返回。SimpleGrantedAuthority是Spring Security中GrantedAuthority接口的具体实现类,和我们自定义的Authority类似,用字符串role保存用户的角色信息,并实现了getAuthority()方法返回角色。

@Entity // 实体
public class User implements UserDetails, Serializable {
	private static final long serialVersionUID = 1L;
	
	private Long id; // 用户的唯一标识
	private String name;
	private String email;
	private String username; // 用户账号,用户登录时的唯一标识
	private String password; // 登录时密码
	private String avatar; // 头像图片地址

	@ManyToMany(cascade = CascadeType.DETACH, fetch = FetchType.EAGER)
	@JoinTable(name = "user_authority", joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"), 
		inverseJoinColumns = @JoinColumn(name = "authority_id", referencedColumnName = "id"))
	private List<Authority> authorities;

	protected User() { // JPA 的规范要求无参构造函数;设为 protected 防止直接使用
	}

	public User(String name, String email,String username,String password) {
		this.name = name;
		this.email = email;
		this.username = username;
		this.password = password;
	}

	public Long getId() {		return id;	}
	public void setId(Long id) {		this.id = id;	}
	public String getName() {		return name;	}
	public void setName(String name) {		this.name = name;	}
	public String getEmail() {		return email;	}
	public void setEmail(String email) {		this.email = email;	}
	
	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		//  将查询结果 List 转换成 List
		List<SimpleGrantedAuthority> simpleAuthorities = new ArrayList<>();
		for(GrantedAuthority authority : this.authorities){
			simpleAuthorities.add(new SimpleGrantedAuthority(authority.getAuthority()));
		}
		return simpleAuthorities;
	}
	public void setAuthorities(List<Authority> authorities) {		this.authorities = authorities;	}
	
	@Override
	public String getUsername() {		return username;	}
	public void setUsername(String username) {		this.username = username;	}
	@Override
	public String getPassword() {		return password;	}
	public void setPassword(String password) {		this.password = password;	}
	public String getAvatar() {		return avatar;	}
	public void setAvatar(String avatar) {		this.avatar = avatar;	}
	@Override
	public boolean isAccountNonExpired() {		return true;	}
	@Override
	public boolean isAccountNonLocked() {		return true;	}
	@Override
	public boolean isCredentialsNonExpired() {		return true;	}
	@Override
	public boolean isEnabled() {		return true;	}
	@Override
	public String toString() {
		return String.format("User[id=%d, username='%s', name='%s', email='%s', password='%s']", id, username, name, email, password);
	}
}

Repository类

权限Authority的Repository,用于完成和数据库的交互,直接继承JPA接口的默认实现即可

public interface AuthorityRepository extends JpaRepository<Authority, Long>{
}

用户User的Repository

public interface UserRepository extends JpaRepository<User, Long> {
    //根据方法名生成查询函数
    User findByUsername(String username);

    //使用Distinct
    List<User> findDistinctByUsername(String username);

    /**
     * 根据用户名分页查询用户列表
     *
     * @param name     名字
     * @param pageable 分页
     * @return 分页用户信息
     */
    Page<User> findByNameLike(String name, Pageable pageable);
}

Service层

定义Authority对应的Service接口与实现类

@Service
public class AuthorityServiceImpl  implements AuthorityService {
	@Autowired
	private AuthorityRepository authorityRepository;
	
	@Override
	public Authority getAuthorityById(Long id) {
		return authorityRepository.getOne(id);
	}
}

定义User对应的Service,通过调用UserRepository实现User的增删改查等操作。此外还需要实现UserDetailsService接口的loadUserByUsername()方法,用于之后权限校验时进行用户查询

@Service
public class UserServiceImpl implements UserService, UserDetailsService {
	@Autowired
	private UserRepository userRepository;
	
	@Transactional
	@Override
	public User saveUser(User user) {
		return userRepository.save(user);
	}

	@Override
	public List<User> listUsers() {
		return userRepository.findAll();
	}
	......

	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		return userRepository.findByUsername(username);
	}

Controller层

Controller层的定义遵循RESTful风格,当通过GET方法访问路径/users时,触发userList()方法,在其中通过userService的listUser()方法获取所有的User信息列表并放到Model中,最后返回页面user/list

在前端页面完成用户增加或修改操作后会发送Post请求到UserController,此时会触发saveOrUpdateUser(),在其中对用户信息和权限进行保存

通过@DeleteMapping注解定义对DELETE请求的响应方法delete(),完成对用户的删除操作

@RestController
@RequestMapping("/users")
@PreAuthorize("hasAuthority('ROLE_ADMIN')")  // 指定角色权限才能操作方法
public class UserController {
    @Autowired
    private UserService userService;

    @Autowired
    private AuthorityService authorityService;

    @GetMapping
    public ModelAndView userList(Model model) {

        Sort sort = Sort.by(Sort.Order.desc("id"));
        Pageable pageable =PageRequest.of(pageIndex,pageSize, sort);

        Page<User> page = userService.listUsers();
        List<User> list = page.getContent();    // 当前所在页面数据列表

        model.addAttribute("page", page);
        model.addAttribute("userList", list);
        return new ModelAndView("user/list", "userModel", model);
    }
    
    @PostMapping
    public ResponseEntity<Response> saveOrUpdateUser(User user, Long authorityId) {
    	//保存用户的权限列表
        List<Authority> authorities = new ArrayList<>();
        authorities.add(authorityService.getAuthorityById(authorityId));
        user.setAuthorities(authorities);

		//保存用户信息
        try {
            userService.saveUser(user);
        } catch (ConstraintViolationException e) {
            return ResponseEntity.ok().body(new Response(false, ConstraintViolationExceptionHandler.getMessage(e)));
        }

        return ResponseEntity.ok().body(new Response(true, "保存成功", user));
    }

	@DeleteMapping(value = "{id}")
    public ResponseEntity<Response> delete(@PathVariable("id") Long id, Model model) {
        System.out.println("delete reach");
        try {
            userService.removeUser(id);
        } catch (Exception e) {
            return ResponseEntity.ok().body(new Response(false, e.getMessage()));
        }
        return ResponseEntity.ok().body(new Response(true, "处理成功"));
    }
}

SecurityConfigure

SecurityConfigure类中对Spring Security的访问权限进行配置

configure()方法中对访问路径的权限进行配置,antMatchers()对访问路径进行匹配,permitAll()代表放行所有对该路径的请求,hasRole("ADMIN")代表只有ADMIN的角色才能访问。formLogin()代表使用表单进行验证,使用loginPage()failureUrl()定义登录、错误页面

configureGlobal()方法中对用户认证信息进行管理。在其中通过userDetailsService()获取用户信息,通过authenticationProvider()获取认证信息,除了使用之前定义的userDetailsService获取用户信息外,还用到passwordEncoder来对密码进行加密

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) // 启用方法级别的安全校验
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    private static final String KEY = "RM_KEY";

    @Qualifier("userServiceImpl")
    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();   // 使用BCrypt加密
    }

    @Bean
    public AuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
        authenticationProvider.setUserDetailsService(userDetailsService);
        authenticationProvider.setPasswordEncoder(passwordEncoder); // 设置密码加密方式
        return authenticationProvider;
    }

    /**
     * 自定义配置
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().antMatchers("/css/**", "/js/**", "/fonts/**", "/index").permitAll() // 都可以访问
                .antMatchers("/h2-console/**").permitAll() // 都可以访问
                .antMatchers("/admins/**").hasRole("ADMIN") // 需要相应的角色才能访问
                .and()
                .formLogin()   //基于 Form 表单登录验证
                .loginPage("/login").failureUrl("/login-error") // 自定义登录界面、登录失败页面
                .and().rememberMe().key(KEY) // 启用 remember me
                .and().exceptionHandling().accessDeniedPage("/403");  // 处理异常,拒绝访问就重定向到 403 页面
        http.csrf().ignoringAntMatchers("/h2-console/**"); // 禁用 H2 控制台的 CSRF 防护
        http.headers().frameOptions().sameOrigin(); // 允许来自同一来源的H2 控制台的请求
    }

    /**
     * 认证信息管理
     */
    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService);	//获取用户认证信息
        auth.authenticationProvider(authenticationProvider());
    }

其中通过@EnableGlobalMethodSecurity(prePostEnabled=true) 开启了方法级别的安全校验,即可以在某个具体方法上添加注解来精确控制是否允许访问。有如下四种方法注解:

  1. @PreAuthorize 在方法调用之前,基于表达式的计算结果来限制对方法的访问
  2. @PostAuthorize允许方法调用,但是如果表达式计算结果为false,将抛出一个安全性异
  3. @PostFilter允许方法调用,但必须按照表达式来过滤方法的结果
  4. @PreFilter 允许方法调用,但必须在进入方法之前过滤输入值
@PreAuthorize("hasRole('ADMIN')")
List<Person> findAll();

Thymeleaf中使用Springsecurity

Thymeleaf中可以引入依赖从而便捷地使用Springsecurity相关特性,通过gradle引入thymeleaf-extras-springsecurity5依赖,注意spring boot2.3.4匹配的版本号为5,版本不匹配无法使用

dependencies {
	//Spring Security
	implementation('org.springframework.boot:spring-boot-starter-security')
	implementation('org.thymeleaf.extras:thymeleaf-extras-springsecurity5')
	...
}

然后在要使用的页面声明spring security标签前缀sec

<html xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">

通过isAuthenticated()可以判断用户是否已经登录,isAnonymous()判断用户未登录,从而可以实现根据用户是否登录显示不同的内容。通过sec:authentication="name"可以获取登录的用户名。

<ul class="nav navbar-nav navbar-right">
    <li sec:authorize="isAuthenticated()" class="active">
        <a href="./">
            <span sec:authentication="name">span>
            <span class="badge">3span>
        a>
    li>
    <li sec:authorize="isAuthenticated()" style="margin-top: 1.5rem;">
        <form th:action="@{/logout}" method="post">
            <input class="btn btn-outline-success " type="submit" value="退出">
        form>
    li>
    <li sec:authorize="isAnonymous()">
        <a href="/login" class="btn btn-outline-success my-2 my-sm-0" type="submit">登录a>
    li>
    <li sec:authorize="isAnonymous()">
        <a href="/register" class="btn btn-outline-success my-2 my-sm-0" type="submit">注册a>
    li>
ul>

如下所示,如果用户已经登陆则显示用户名和退出按钮,若未登录则显示登录和注册
在这里插入图片描述在这里插入图片描述类似地,在Spring boot中可以通过SecurityContextHolder获取当前登录的用户信息

User user= (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();

值得注意的是Spring security会自动实现对/login、/logout请求的处理,例如下面的登陆页面发送Post请求到@{/login},Spring Security会对用户名密码进行判断,如果登录成功会跳转到请求的页面,否则跳转到/login-error。此外还可以通过name="remember-me"的checkbox实现记住我的功能,关闭浏览器再次访问无需重新登录。发送Post请求到@{/logout}会注销用户并跳转到登陆页面。

<form  th:action="@{/login}" method="post">
    <h2 >请登录h2>
    <div class="form-group col-md-5">
        <label for="username" class="col-form-label">账号label>
        <input type="text" class="form-control" id="username" name="username" maxlength="50" placeholder="请输入账号">
    div>
    <div class="form-group col-md-5">
        <label for="password" class="col-form-label">密码label>
        <input type="password" class="form-control" id="password" name="password" maxlength="30" placeholder="请输入密码" >
    div>
    <div class="form-group col-md-2">
        <input type="checkbox" name="remember-me"/> 记住我
    div>
    <div class="form-group col-md-5">
        <button type="submit" class="btn btn-primary">登录button>
    div>
    <div class=" col-md-5" th:if="${loginError}">
        <p class="blog-label-error" th:text="${errorMsg}">p>
    div>
form>

CSRF防护

在上面的configure()中通过http.csrf()开启了网站的CSFR防护,CSRF全称为跨站点请求伪造(Cross—Site Request Forgery),即当用户A在通过网页安全验证后访问B网站,并将校验信息保存到本地cookie,之后黑客通过诱导A访问C页面后再去访问B网站,这样C就可以使用A本地的校验信息通过B的安全验证,从而以A的身份进行危险操作。

CSRF 攻击之所以能够成功,是因为黑客可以完全伪造用户的请求,该请求中所有的用户验证信息都是存在于 cookie 中,因此黑客可以在不知道这些验证信息的情况下直接利用用户自己的 cookie 来通过安全验证。要抵御 CSRF,关键在于在请求中放入黑客所不能伪造的信息,并且该信息不存在于 cookie 之中。可以在 HTTP 请求中以参数的形式加入一个随机产生的 token,并在服务器端验证这个 token,如果请求中没有 token 或者 token 内容不正确,则认为可能是 CSRF 攻击而拒绝该请求。

对于 GET 请求,token 将附在请求地址之后,这样 URL 就变成 http://url?csrftoken=tokenvalue。 而对于 POST 请求来说,要在 form 的最后加上 ,Thymeleaf模板引擎会自动在form表单后添加csrf校验字段。但是我们通过Ajax发送请求时就需要手动添加校验信息了。
如下所示,首先在页面模板中添加csfr字段,这样所有页面公用头部模板就都添加了

<head th:fragment="static">
    
    <meta name="_csrf" th:content="${_csrf.token}"/>
    
    <meta name="_csrf_header" th:content="${_csrf.headerName}"/>
    ...
head>

接着在js中获取头部的csrf内容,并且在发送Ajax请求时将其添加到请求头即可通过CSRF验证

    // 删除用户
    $("#mainContainer").on("click", ".blog-delete-user", function () {
        // 获取 CSRF Token
        const csrfToken = $("meta[name='_csrf']").attr("content");
        const csrfHeader = $("meta[name='_csrf_header']").attr("content");

        const deleteUrl = projectName + "/users/" + $(this).attr("userId")

        $.ajax({
            url: deleteUrl,
            type: 'DELETE',
            beforeSend: function (request) {
                request.setRequestHeader(csrfHeader, csrfToken); // 在请求头添加CSRF Token
            },
            success: function (data) {
                if (data.success) {
                    getUersByName(0, _pageSize);    //删除成功刷新主界面
                } else {
                    console.log("del fail");
                }
            }
        });
    });
});

你可能感兴趣的:(Java,Spring,Security,用户权限管理)