Spring Security 是 Spring 社区的一个顶级项目,也是 Spring Boot 官方推荐使用的安全框架。除了常规的认证(Authentication)和授权(Authorization)之外,Spring Security还提供了诸如ACLs,LDAP,JAAS,CAS等高级特性以满足复杂场景下的安全需求。
Spring Security 应用级别的安全主要包含两个主要部分,即登录认证(Authentication)和访问授权(Authorization),首先用户登录的时候传入登录信息,登录验证器完成登录认证并将登录认证好的信息存储到请求上下文,然后在进行其他操作,如接口访问、方法调用时,权限认证器从上下文中获取登录认证信息,然后根据认证信息获取权限信息,通过权限信息和特定的授权策略决定是否授权。
Spring Security 的核心就是filter,通过一层层的filter后,才访问到我们的资源信息。Spring Security 的filter做着一层一层的拦截,把相关的权限做一层一层的验证。成功?走下层。失败?认证失败。Spring Security的所有的权限校验都是这样做的,一切的认证都在filter中,业务代码完全不知情。
Spring Security 的工作流程图:
完整代码地址:https://github.com/mer97/spring-security5-demo/tree/master
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-mongodb'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.session:spring-session-core'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.0.4.RELEASE'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.apache.commons:commons-lang3:3.8.1'
implementation 'commons-codec:commons-codec:1.11'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
}
/**
* Spring Security 权限认证配置类。
*
* @author LEEMER
* Create Date: 2019-05-21
*/
@Configuration
@Order(2)
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class UserSecurityConfig extends WebSecurityConfigurerAdapter {
private static final Logger LOGGER = LoggerFactory.getLogger(UserSecurityConfig.class);
/**
* 用户数据仓库。
*/
private UserRepository userRepository;
/**
* json格式转换类。
*/
private ObjectMapper objectMapper;
/**
* ajax请求失败处理器。
*/
private AjaxAuthFailureHandler ajaxAuthFailureHandler;
public UserSecurityConfig(UserRepository userRepository,
ObjectMapper objectMapper,
AjaxAuthFailureHandler ajaxAuthFailureHandler) {
this.userRepository = userRepository;
this.objectMapper = objectMapper;
this.ajaxAuthFailureHandler = ajaxAuthFailureHandler;
}
/**
* 使用Thymeleaf的Spring Security方言
* @return
*/
@Bean
public SpringSecurityDialect springSecurityDialect() {
return new SpringSecurityDialect();
}
/**
* 用于执行密码的单向转换,以便安全地存储密码,可自定义加密方法。
* @return
*/
@Bean
@Order(1)
public PasswordEncoder md5PasswordEncoderForUser() {
return new PasswordEncoder() {
@Override
public String encode(CharSequence rawPassword) {
return rawPassword.toString();
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return encodedPassword.equals(encode(rawPassword));
}
};
}
/**
* 验证用户名、密码和授权。
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetailsService userDetailsService() throws UsernameNotFoundException {
return (username) -> {
var user = userRepository.findByUsername(username).orElse(null);
if (user == null) {
throw new UsernameNotFoundException("User Not Found: " + username);
}
return User.withUsername(username)
.password(user.getPassword())
.authorities(user.getRoles().stream()
.filter(Objects::nonNull)
.map(Role::getAuthorities)
.filter(Objects::nonNull)
.flatMap(Collection::stream)
.filter(Objects::nonNull)
.map(Authority::toString)
.map(SimpleGrantedAuthority::new)
.toArray(SimpleGrantedAuthority[]::new))
.build();
};
}
/**
* 配置自定义验证用户名、密码和授权的服务。
* @param authenticationManagerBuilder
* @throws Exception
*/
@Override
public void configure(AuthenticationManagerBuilder authenticationManagerBuilder)
throws Exception {
authenticationManagerBuilder.userDetailsService(userDetailsService());
}
/**
* http请求配置:
* 1.开启权限。
* 2.释放资源配置。
* 3.登录请求配置。
* 5.退出登录配置。
* 5.开启csrf防护。
* @param http
* @throws Exception
*/
@Override
public void configure(HttpSecurity http) throws Exception {
http.exceptionHandling()
.authenticationEntryPoint(unauthorizedEntryPoint())
.accessDeniedHandler(handleAccessDeniedForUser())
.and()
.headers()
.frameOptions()
.disable()
.and()
.authorizeRequests()
.antMatchers("/public/**","/login")
.permitAll()
.anyRequest()
.hasAuthority("DSC_USER")
.and()
.formLogin()
.loginPage("/login")
.loginProcessingUrl("/api/v1/login")
.permitAll()
.defaultSuccessUrl("/admin")
.successHandler(ajaxAuthSuccessHandler())
.failureHandler(ajaxAuthFailureHandler)
.and()
.logout()
.logoutUrl("/api/v1/logout")
.logoutSuccessHandler(ajaxLogoutSuccessHandler())
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID");
}
/**
* 判断是否ajax请求,是ajax请求则返回json,否则跳转失败页面。
* @return
*/
private AuthenticationEntryPoint unauthorizedEntryPoint() {
return (HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) -> {
var requestedWithHeader = request.getHeader("X-Requested-With");
if ("XMLHttpRequest".equals(requestedWithHeader)) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json");
response.getOutputStream().write(authException.getMessage().getBytes());
} else {
response.sendRedirect("/login");
}
};
}
/**
* 自定义登录成功处理器。
* @return
*/
private AuthenticationSuccessHandler ajaxAuthSuccessHandler() {
return (HttpServletRequest request, HttpServletResponse response, Authentication authentication) -> {
response.setStatus(HttpServletResponse.SC_OK);
response.setContentType("application/json");
var root = objectMapper.createObjectNode();
root.put("redirect",
request.getRequestURI().equals("/api/v1/login") ? "/admin" : request.getRequestURI());
response.getOutputStream().write(root.toString().getBytes());
};
}
/**
* 自定义注销成功处理器。
* @return
*/
private LogoutSuccessHandler ajaxLogoutSuccessHandler() {
return (HttpServletRequest request, HttpServletResponse response, Authentication authentication) -> {
response.setStatus(HttpServletResponse.SC_OK);
response.setContentType("application/json");
var root = objectMapper.createObjectNode();
root.put("redirect", "/login");
response.getOutputStream().write(root.toString().getBytes());
};
}
/**
* 自定义AccessDeniedHandler来处理Ajax请求。
* @return
*/
private AccessDeniedHandler handleAccessDeniedForUser() {
return (HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException accessDeniedException) -> {
var requestedWithHeader = request.getHeader("X-Requested-With");
if ("XMLHttpRequest".equals(requestedWithHeader)) {
var errorResponse = new ErrorResponse(accessDeniedException.getMessage());
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.setContentType("application/json");
response.getOutputStream().write(objectMapper.writeValueAsBytes(errorResponse));
} else {
response.sendRedirect("/login");
}
};
}
}
/**
* 用户实体类。
*
* @author LEEMER
* Create Date: 2019-05-21
*/
@Document("user")
public class User {
@Id
@Null(groups = UserCreator.class, message = "“用户ID”必须为空")
private ObjectId id;
// @NotBlank(groups = UserLogin.class, message = "“用户名”不能为空”")
private String username;
@NotBlank(groups = UserCreator.class, message = "“密码”不能为空”")
// @Pattern(regexp = "(?=.*?[A-Z])(?=.*?[0-9])(?=.*[a-z])",
// message = "“密码”必须包含大小写字母和数字")
@Length(groups = UserCreator.class, min = 8, message = "“密码”不能少于8位")
private String password;
// @NotBlank(groups = UserCreator.class, message = "“二级密码”不能为空”")
// @Pattern(regexp = "(?=.*?[A-Z])(?=.*?[0-9])(?=.*[a-z])",
// message = "“二级密码”必须包含大小写字母和数字")
// @Length(groups = UserCreator.class, min = 8, message = "“二级密码”不能少于8位")
// private String secondaryPassword;
@NotBlank(groups = UserCreator.class, message = "“邮箱”不能为空”")
@Pattern(regexp = "^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$",
message = "“邮箱”输入有误")
private String mail;
private String phone;
/**
* 密码更新时间。
*/
@Field("password_updated_time")
private Long passwordUpdatedTime;
/**
* 创建时间。
*/
@Field("create_time")
private Long createTime;
private List roles;
public ObjectId getId() {
return id;
}
public void setId(ObjectId id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getMail() {
return mail;
}
public void setMail(String mail) {
this.mail = mail;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public Long getPasswordUpdatedTime() {
return passwordUpdatedTime;
}
public void setPasswordUpdatedTime(Long passwordUpdatedTime) {
this.passwordUpdatedTime = passwordUpdatedTime;
}
public Long getCreateTime() {
return createTime;
}
public void setCreateTime(Long createTime) {
this.createTime = createTime;
}
public List getRoles() {
return roles;
}
public void setRoles(List roles) {
this.roles = roles;
}
}
/**
* 角色实体类。
*
* @author LEEMER
* Create Date: 2019-05-21
*/
@Document("role")
public class Role {
@Id
private ObjectId id;
private String name;
private String description;
private List authorities;
public ObjectId getId() {
return id;
}
public void setId(ObjectId id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public List getAuthorities() {
return authorities;
}
public void setAuthorities(List authorities) {
this.authorities = authorities;
}
}
/**
* 权限模块枚举。
*
* @author LEEMER
* Create Date: 2019-05-21
*/
public enum Authority {
DSC_USER("DSC-用户"),
DSC_ADMIN("DSC-超级管理员");
private String description;
Authority(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
}
/**
* 自定义验证失败处理器。
*
* @author LEEMER
* Create Date: 2019-05-21
*/
@Component
public class AjaxAuthFailureHandler implements AuthenticationFailureHandler {
private ObjectMapper objectMapper;
public AjaxAuthFailureHandler(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
@Override
public void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
var errorResponse = new ErrorResponse("用户名或密码错误");
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.setContentType("application/json");
response.getOutputStream().write(objectMapper.writeValueAsBytes(errorResponse));
}
}
/**
* 服务执行异常的返回对象。
*
* @author LEEMER
* Create Date: 2019-05-21
*/
public class ErrorResponse {
private String error;
public ErrorResponse(String error) {
this.error = error;
}
public String getError() {
return error;
}
public void setError(String error) {
this.error = error;
}
}
/**
* @author LEEMER
* Create Date: 2019-05-21
*/
@Controller
public class BaseController {
/**
* 跳转登录页面
* @return
*/
@RequestMapping("/login")
public String login() {
return "web/login";
}
/**
* @PreAuthorize("hasAuthority('DSC_ADMIN')")
* 权限验证:
* 当请求/admin接口时,判断该用户是否拥有“DSC_ADMIN”权限。
*
* @return
*/
@GetMapping("/admin")
@PreAuthorize("hasAuthority('DSC_ADMIN')")
@ResponseBody
public String admin() {
System.out.println("admin");
return "This is admin view.";
}
}
/**
* 全局js异常处理。
*/
$(document).ajaxError(function (event, jqxhr, settings, thrownError) {
alert(JSON.parse(jqxhr.responseText)['error']);
});
/**
* 全局CSRF设置。
*/
$(function () {
var token = $("meta[name='_csrf']").attr("content");
var header = $("meta[name='_csrf_header']").attr("content");
$(document).ajaxSend(function(e, xhr, options) {
xhr.setRequestHeader(header, token);
});
});
//开启csrf防护
用户登录
有时候视图上的一部分内容需要根据用户被授予了什么权限来确定是否渲染。Spring Security的标签能够根据用户被授予的权限有条件地渲染页面的部分内容。下面是一个简单的示例:
首页
完整代码地址:https://github.com/mer97/spring-security5-demo/tree/master