简介
一个SpringBoot的社区项目。
使用SpringSecurity完成登录认证和权限控制。
配置类
在5.7版本的SpringSecurity中,WebSecurityConfigurerAdapter已经Deprecated,我使用2.1.5.RELEASE版本的SpringSecurity,所以继续使用WebSecurityConfigurerAdapter类。
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter
Provides a convenient base class for creating a
WebSecurityConfigurerinstance. The implementation allows customization by overriding methods.WebSecurityConfigurerAdapter (spring-security-docs 5.7.2 API)
In Spring Security 5.7.0-M2 we deprecated the
WebSecurityConfigurerAdapter, as we encourage users to move towards a component-based security configuration.
configure(WebSecurity web)
| Modifier and Type | Method | Description |
|---|---|---|
void | configure(WebSecurity web) | Override this method to configure WebSecurity. |
忽略对静态资源的拦截
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/resources/**");
}
configure(HttpSecurity http)
| Modifier and Type | Method | Description |
|---|---|---|
protected void | configure(HttpSecurity http) | Override this method to configure the HttpSecurity. |
重写configure(HttpSecurity http)方法,我们主要的配置都是在这个方法中完成。
//授权
http.authorizeRequests()
.antMatchers(
"/user/setting",
"/user/upload",
"/discuss/add",
"/comment/add/**",
"/letter/**",
"/notice/**",
"/like",
"/follow",
"/unfollow"
)
.hasAnyAuthority(
AUTHORITY_USER,
AUTHORITY_ADMIN,
AUTHORITY_MODERATOR
)
.antMatchers(
"/discuss/top",
"/discuss/wonderful"
)
.hasAnyAuthority(
AUTHORITY_MODERATOR
)
.antMatchers(
"/discuss/delete",
"/data/**"
)
.hasAnyAuthority(
AUTHORITY_ADMIN
)
.anyRequest().permitAll()//其他请求都允许
.and().csrf().disable();//不启用csrf
antMatchers中添加路径,hasAnyAuthority中添加可以访问对应路径的用户类型。
-
普通用户
AUTHORITY_USER、版主AUTHORITY_MODERATOR、管理员AUTHORITY_ADMIN可以进行用户设置/user/setting、发帖/discuss/add等操作 -
版主
AUTHORITY_MODERATOR可以置顶/discuss/top、加精/discuss/wonderful -
管理员
AUTHORITY_ADMIN可以删帖/discuss/delete、查看网站数据/data/**
关于CSRF(跨站点请求伪造)
- 使用SpringSecurity后,生成页面时页面可从服务器获取到
_csrf.token和_csrf.headerName。 - 发送AJAX请求之前,将CSRF令牌设置到请求的消息头中,
_csrf.headerName作key,_csrf.token作value,效果就是请求头中多了一个字段X-CSRF-TOKEN: 1fef99fe-11cf-12e4-dg32-235236199a3f,字段值为举例。 - 服务器通过校验
X-CSRF-TOKEN字段,判断本次请求是否可信。
- 为系统中的每一个连接请求加上一个token,这个token是随机的,服务端对该token进行验证。破坏者在留言或者伪造嵌入页面的时候,无法预先判断CSRF token的值是什么,所以当服务端校验CSRF token的时候也就无法通过。所以这种方法在一定程度上是靠谱的。
由于CSRF的本质在于攻击者欺骗用户去访问自己设置的地址,所以如果要求在访问敏感数据请求时,要求用户浏览器提供不保存在cookie中,并且攻击者无法伪造的数据作为校验,那么攻击者就无法再运行CSRF攻击。这种数据通常是窗体中的一个数据项。服务器将其生成并附加在窗体中,其内容是一个伪随机数。当客户端通过窗体提交请求时,这个伪随机数也一并提交上去以供校验。正常的访问时,客户端浏览器能够正确得到并传回这个伪随机数,而通过CSRF传来的欺骗性攻击中,攻击者无从事先得知这个伪随机数的值,服务端就会因为校验token的值为空或者错误,拒绝这个可疑请求。
- 没有登录
- 异步请求:返回json格式的提示
- 普通请求:重定向到登录页面
- 权限不足
- 异步请求:返回json格式的提示
- 普通请求:重定向到404页面
// 权限不够时的处理
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");
}
}
});
异步请求的请求头有x-requested-with字段,且内容为XMLHttpRequest,通过请求头区分异步请求与普通请求。
Security底层默认会拦截/logout请求,进行退出处理。我们用/securitylogout覆盖它默认的逻辑,访问/logout时就会走我们自己写的逻辑。
http.logout().logoutUrl("/securitylogout");
@RequestMapping(path = "/logout", method = RequestMethod.GET)
public String logout(@CookieValue("ticket") String ticket) {
userService.logout(ticket);
SecurityContextHolder.clearContext();
return "redirect:/login";
}
不使用SpringSecurity完成登录控制,使未登录的用户不能使用用户设置、发帖等功能。
- 自定义主注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginRequired {
}
- 给需要登录才能访问的功能添加
@LoginRequired注解,如用户设置、发帖 - 写拦截器,用户访问的如果是有
@LoginRequired注解的功能,必须登录后才能使用
@Component
public class LoginRequiredInterceptor implements HandlerInterceptor {
@Autowired
private HostHolder hostHolder;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
LoginRequired loginRequired = method.getAnnotation(LoginRequired.class);
if (loginRequired != null && hostHolder.getUser() == null) {
//有LoginRequired注解且没有登录
response.sendRedirect(request.getContextPath() + "/login");//重定向到登录页面
return false;
}
}
return true;
}
}
拦截器
preHandle方法中- 从cookie中获取
ticket - 验证
ticket - 根据
ticket获取用户 - 用户信息存入
hostHolder - 用户信息存入
SecurityContextHolder
- 从cookie中获取
afterCompletion方法中hostHolder.clear();SecurityContextHolder.clearContext();
@Component
public class LoginTicketInterceptor implements HandlerInterceptor {
@Autowired
private UserService userService;
@Autowired
private HostHolder hostHolder;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//从cookie中获取凭证
String ticket = CookieUtil.getValue(request, "ticket");
if (ticket == null) {
return true;
}
LoginTicket loginTicket = userService.getLoginTicket(ticket);
//查询凭证是否有效
if (loginTicket == null
|| loginTicket.getStatus() != 0
|| !loginTicket.getExpired().after(new Date())) {
return true;
}
User user = userService.getUser(loginTicket.getUserId());
//在本次请求中持有用户
hostHolder.setUser(user);
//构建用户认证的结果,并存入securityContext,以便以security进行授权
Authentication authentication = new UsernamePasswordAuthenticationToken(
user, user.getPassword(), userService.getAuthorities(user.getId())
);
SecurityContextHolder.setContext(new SecurityContextImpl(authentication));
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
if (modelAndView != null && hostHolder.getUser() != null) {
modelAndView.addObject("loginUser", hostHolder.getUser());
}
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
hostHolder.clear();
SecurityContextHolder.clearContext();
}
}
Associates a given
SecurityContextwith the current execution thread.
Modifier and Type Method and Description static voidclearContext()Explicitly clears the context value from the current thread.static voidsetContext(SecurityContext context)Associates a newSecurityContextwith the current thread of execution.
类UserServiceImpl中根据用户id获取对应的权限的方法。
public Collection<? extends GrantedAuthority> getAuthorities(int userId) {
User user = this.getUser(userId);
List<GrantedAuthority> list = new ArrayList<>();
list.add(new GrantedAuthority() {
@Override
public String getAuthority() {
switch (user.getType()) {
case 1:
return AUTHORITY_ADMIN;
case 2:
return AUTHORITY_MODERATOR;
default:
return AUTHORITY_USER;
}
}
});
return list;
}
JWT
以后使用JWT改进登录