title: Spring Security
date: 2019-08-05 16:40:27
categories:
- 后端
tags: - 后端
- 权限管理
Spring Security
- Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架(简单说是对访问权限进行控制嘛)。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
- 核心:认证和授权
- RBAC基于角色的访问控制
- 安全配置类,集成WebSecurityConfigurerAdapter
package com.waylau.spring.boot.blog.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* Spring Security 配置类.
*
* @since 1.0.0 2017年3月8日
* @author <a href="https://waylau.com">Way Lau</a>
*/
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) // 启用方法安全设置
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private static final String KEY = "waylau.com";
@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;
}
/**
* 自定义配置,设置访问路径,并且静止h2控制台的csrf防护
*/
@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 控制台的请求
}
/**
* 认证信息管理
* @param auth
* @throws Exception
*/
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
auth.authenticationProvider(authenticationProvider());
}
}
-
html代码示例
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4"> <head> </head> <body> <div sec:authorize="isAuthenticated()">这里就是判断是否认证的样子 <p>已登录</p> <p>登录名:<span sec:authentication="name"></span></p> <p>Password:<span sec:authentication="principal.password"></span></p> <div sec:authentication="principal.authorities"></div> <!-- works fine --> <p>Email :<span sec:authentication="principal.email"></span></p> <p>Name:<span sec:authentication="principal.username"></span></p> <p>Status:<span sec:authentication="principal.status"></span></p> <p>拥有的角色: <span sec:authorize="hasRole('ROLE_ADMIN')">管理员</span> <span sec:authorize="hasRole('ROLE_USER')">用户</span> </p> </div> <div sec:authorize="isAnonymous"><p>未登录</p></div> </body> </html>
-
有一个问题需要注意,如果你是ADMIN,在数据库里面必须储存ROLE_ADMIN,其他云云。
-
同时,在security中,角色和权限共用GrantedAuthorty接口,唯一不同就是角色有个前缀ROLE_,
package org.springframework.security.core.authority; import org.springframework.security.core.GrantedAuthority; import org.springframework.util.Assert; public final class SimpleGrantedAuthority implements GrantedAuthority { private static final long serialVersionUID = 500L; private final String role; public SimpleGrantedAuthority(String role) { Assert.hasText(role, "A granted authority textual representation is required"); this.role = role; } public String getAuthority() { return this.role; } public boolean equals(Object obj) { if (this == obj) { return true; } else { return obj instanceof SimpleGrantedAuthority ? this.role.equals(((SimpleGrantedAuthority)obj).role) : false; } } public int hashCode() { return this.role.hashCode(); } public String toString() { return this.role; } }
-
实例操作,假如用户和权限分开
package com.waylau.spring.boot.blog.domain; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import org.springframework.security.core.GrantedAuthority; /** * 权限. */ @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 name; public Long getId() { return id; } public void setId(Long id) { this.id = id; } /* * (non-Javadoc) * * @see org.springframework.security.core.GrantedAuthority#getAuthority() */ @Override public String getAuthority() { return name; } public void setName(String name) { this.name = name; } } package com.waylau.spring.boot.blog.domain; import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import java.util.List; import javax.persistence.CascadeType; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.JoinTable; import javax.persistence.ManyToMany; import javax.validation.constraints.Size; import org.hibernate.validator.constraints.Email; import org.hibernate.validator.constraints.NotEmpty; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; /** * User 实体 * * @since 1.0.0 2017年3月5日 * @author <a href="https://waylau.com">Way Lau</a> */ @Entity // 实体 public class User implements UserDetails, Serializable { //springsecurity要求的实现方法 private static final long serialVersionUID = 1L; @Id // 主键 @GeneratedValue(strategy = GenerationType.IDENTITY) // 自增长策略 private Long id; // 用户的唯一标识 @NotEmpty(message = "姓名不能为空") @Size(min=2, max=20) @Column(nullable = false, length = 20) // 映射为字段,值不能为空 private String name; @NotEmpty(message = "邮箱不能为空") @Size(max=50) @Email(message= "邮箱格式不对" ) @Column(nullable = false, length = 50, unique = true) private String email; @NotEmpty(message = "账号不能为空") @Size(min=3, max=20) @Column(nullable = false, length = 20, unique = true) private String username; // 用户账号,用户登录时的唯一标识 @NotEmpty(message = "密码不能为空") @Size(max=100) @Column(length = 100) private String password; // 登录时密码 @Column(length = 200) 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; } //用户权限实现信息 public Collection<? extends GrantedAuthority> getAuthorities() { // 需将 List<Authority> 转成 List<SimpleGrantedAuthority>,否则前端拿不到角色列表名称 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 void setEncodePassword(String password) { PasswordEncoder encoder = new BCryptPasswordEncoder(); String encodePasswd = encoder.encode(password); this.password = encodePasswd; } public String getAvatar() { return avatar; } public void setAvatar(String avatar) { this.avatar = avatar; } //改成true方法, @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); } } 请看User实现了UserDetail方法里面有很多方法,有获得用户名和密码的方法,还有权限方法,所以推测UserDetails的实现类就是验证对的方式
-
主要类,用来验证密码的类
public void configure(AuthenticationManagerBuilder auth) throws Exception { if (auth.isConfigured()) { return; } UserDetailsService userDetailsService = getBeanOrNull( UserDetailsService.class); if (userDetailsService == null) { return; } PasswordEncoder passwordEncoder = getBeanOrNull(PasswordEncoder.class); DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); provider.setUserDetailsService(userDetailsService); if (passwordEncoder != null) { provider.setPasswordEncoder(passwordEncoder); } auth.authenticationProvider(provider); }
-
config类示例
package com.waylau.spring.boot.blog.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 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.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; /** * Spring Security 配置类. * * @since 1.0.0 2017年3月8日 * @author <a href="https://waylau.com">Way Lau</a> */ @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) // 启用方法安全设置 public class SecurityConfig extends WebSecurityConfigurerAdapter { private static final String KEY = "waylau.com"; @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 控制台的请求 } /** * 认证信息管理 * @param auth * @throws Exception */ @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService); auth.authenticationProvider(authenticationProvider()); } }