目录
一、Spring Security简介
二、入门实现登录的认证和授权
三、处理 CSRF 攻击
四、thymeleaf + spring security
Spring Security是一个专注于为Java应用程序提供身份认证和授权的框架,是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
spring security 的核心功能主要包括:
具体代码可在码云拷贝:项目源码
【在pom.xml文件导入依赖】
org.springframework.boot
spring-boot-starter-security
导入security依赖后,会立刻对项目产生影响,再次访问项目的任何页面都需要登录,登录账号默认为:user,在日志中给出随机密码
【User实体类实现UserDetails接口】
在用户的实体类User中继承UserDetails接口,并实现下面五个方法:
// true: 账号未过期.
@Override
public boolean isAccountNonExpired() {
return true;
}
// true: 账号未锁定.
@Override
public boolean isAccountNonLocked() {
return true;
}
// true: 凭证未过期.
@Override
public boolean isCredentialsNonExpired() {
return true;
}
// true: 账号可用.
@Override
public boolean isEnabled() {
return true;
}
// 获取用户权限
@Override
public Collection extends GrantedAuthority> getAuthorities() {
List list = new ArrayList<>();
list.add(new GrantedAuthority() {
@Override
public String getAuthority() {
// type表示自定义用户的身份, 1是管理员, 其余是普通用户
switch (type) {
case 1:
return "ADMIN";
default:
return "USER";
}
}
});
return list;
}
【在UserService中实现UserDetailsService方法】
在用户的业务逻辑中实现UserDetailsService方法,并实现loadUserByUsername方法:
@Service
public class UserService implements UserDetailsService {
@Autowired
private UserMapper userMapper;
public User findUserByName(String username) {
return userMapper.selectByName(username);
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return this.findUserByName(username);
}
}
【创建SecurityConfig配置类, 实现过滤】
配置类继承WebSecurityConfigurerAdapter类,并重写 configuer 方法,需要重写三种重载形式的configure
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserService userService;
@Override
public void configure(WebSecurity web) throws Exception {
// 忽略静态资源的访问
web.ignoring().antMatchers("/resource/**");
}
// AuthenticationManager: 认证的核心接口.
// AuthenticationManagerBuilder: 用于构建AuthenticationManager对象的工具.
// ProviderManager: AuthenticationManager接口的默认实现类.
// 认证
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 1. 内置的认证规则
// auth.userDetailsService(userService).passwordEncoder(new Pbkdf2PasswordEncoder("12345"));
// 2. 自定义认证规则
// AuthenticationProvider: ProviderManager将持有一组AuthenticationProvider, 每个AuthenticationProvider负责一种认证.
// 委托模式: ProviderManager将认证委托给AuthenticationProvider.
auth.authenticationProvider(new AuthenticationProvider() {
// Authentication: 用于封装认证信息(账号密码等)的接口, 不同的实现类代表不同类型的认证信息.
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName();
String password = (String) authentication.getCredentials();
User user = userService.findUserByName(username);
if(user == null) {
throw new UsernameNotFoundException("账号不存在!");
}
password = CommunityUtil.md5(password + user.getSalt());
if(!password.equals(user.getPassword())) {
throw new BadCredentialsException("密码不正确!");
}
// principal: 认证的主要信息; credentials: 证书; authorities: 权限
return new UsernamePasswordAuthenticationToken(user, user.getPassword(), user.getAuthorities());
}
// 当前的AuthenticationProvider支持哪种类型的认证.
@Override
public boolean supports(Class> aClass) {
// UsernamePasswordAuthenticationToken: Authentication接口的常用的实现类(账号密码).
return UsernamePasswordAuthenticationToken.class.equals(aClass);
}
});
}
// 授权
@Override
protected void configure(HttpSecurity http) throws Exception {
// 登录相关配置
http.formLogin()
.loginPage("/loginpage")// 登录时的页面
.loginProcessingUrl("/login")// 发送登录请求的路径
.successHandler(new AuthenticationSuccessHandler() {
// 登录成功的处理
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.sendRedirect(request.getContextPath() + "/index");
}
})
.failureHandler(new AuthenticationFailureHandler() {
// 登陆失败的操作
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
request.setAttribute("error", e.getMessage());
request.getRequestDispatcher("/loginpage").forward(request, response);
}
});
// 退出相关配置
http.logout()
.logoutUrl("/logout")// 退出登录的路径
.logoutSuccessHandler(new LogoutSuccessHandler() {
// 退出成功的处理
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.sendRedirect(request.getContextPath() + "/index");
}
});
// 授权配置
http.authorizeRequests()
.antMatchers("/letter").hasAnyAuthority("USER", "ADMIN")// 为指定页面配置指定权限
.antMatchers("/admin").hasAnyAuthority("ADMIN")
.and().exceptionHandling().accessDeniedPage("/denied");// 访问失败后要去的路径
// 增加Filter, 处理验证码
http.addFilterBefore(new Filter() {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
if(request.getServletPath().equals("/login")) {
// 如果是登录页面, 才处理验证码
String verifyCode = request.getParameter("verifyCode");
if(verifyCode == null || !verifyCode.equals("12345")) {
// 验证码不正确
request.setAttribute("error", "验证码错误!");
request.getRequestDispatcher("/loginpage").forward(request, response);
return;
}
}
// 让请求继续向下执行
filterChain.doFilter(request, response);
}
}, UsernamePasswordAuthenticationFilter.class);
// 记住我
http.rememberMe()
.tokenRepository(new InMemoryTokenRepositoryImpl())
.tokenValiditySeconds(3600 * 24)
.userDetailsService(userService);
/*
// 权限不够时的处理
http.exceptionHandling()
.authenticationEntryPoint(new AuthenticationEntryPoint() {
// 没有登录时的处理
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
// 判断请求是异步请求还是同步请求
String xRequestedWith = request.getHeader("x-requested-with");
if("XMLHttpRequest".equals(xRequestedWith)) {
response.setContentType("application/plain;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.write(CommunityUtil.getJSONString(403, "你还没有登录哦!"));
} else {
response.sendRedirect(request.getContextPath() + "/login");
}
}
})
.accessDeniedHandler(new AccessDeniedHandler() {
// 没有权限时的处理
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
// 判断请求是异步请求还是同步请求
String xRequestedWith = request.getHeader("x-requested-with");
if("XMLHttpRequest".equals(xRequestedWith)) {
response.setContentType("application/plain;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.write(CommunityUtil.getJSONString(403, "你没有访问此功能的权限"));
} else {
response.sendRedirect(request.getContextPath() + "/denied");
}
}
});
// Security底层默认会拦截/logout路径请求, 进行退出的处理
// 覆盖它默认的逻辑, 才能执行我们自己退出的代码
http.logout().logoutUrl("/securitylogout");
*/
}
}
认证成功后,结果会通过SecurityContextHolder存入SecurityContext中.
@RequestMapping(path = "/index", method = RequestMethod.GET)
public String getIndexPage(Model model) {
// 认证成功后,结果会通过SecurityContextHolder存入SecurityContext中.
Object obj = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if(obj instanceof User) {
model.addAttribute("loginUser", obj);
}
return "/index";
}
csrf攻击:当用户提交表单数据时,可能会收到csrf的攻击,获取表单中的登录信息,从而盗取用户信息。
Spring Security默认对普通请求的表单的隐藏域中加上csrf的检验序列(令牌),而csrf病毒攻击时不持有令牌将会被拒绝访问
但对于AJAX请求,Spring Security没有默认的检验序列,所以需要我们自动配置。
对AJAX请求配置检验序列:
在js文件的发布内容的事件中将CSRF令牌设置到请求的消息头中:
授权时加上http.csrf().disable();可以废弃Spring Security对csrf攻击的保护。
注:
在 SecurityConfig 类的配置中,若没有使用 Filter 过滤器认证用户信息,只是授权用户无法实现过滤功能,需要在 Interceptor 中拦截用户并构建用户的认证信息,然后手动地存入 SecurityContext 中,以便 Security 进行授权,而在 SecurityConfig 类就不用再进行认证了,只需授权就可以了。
权限管理可以分为两个层面:
这里,thymeleaf 内置的标签支持 spring security,但若想使用其功能还需要导入相关包,选择对应父pom中的版本。
!-- https://mvnrepository.com/artifact/org.thymeleaf.extras/thymeleaf-extras-springsecurity5 -->
org.thymeleaf.extras
thymeleaf-extras-springsecurity5