1.导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2.废弃原来的登录拦截器
import com.nowcoder.community.interceptor.LoginRequiredInterceptor;
import com.nowcoder.community.interceptor.LoginTicketInterceptor;
import com.nowcoder.community.interceptor.MessageInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private LoginTicketInterceptor loginTicketInterceptor;
//@Autowired
//private LoginRequiredInterceptor loginRequiredInterceptor;
@Autowired
private MessageInterceptor messageInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginTicketInterceptor)
.excludePathPatterns("/**/*.css","/**/*.js","/**/*.jpg","/**/*.png","/**/*.jpeg");
//registry.addInterceptor(loginRequiredInterceptor)
//.excludePathPatterns("/**/*.css","/**/*.js","/**/*.jpg","/**/*.png","/**/*.jpeg");
registry.addInterceptor(messageInterceptor)
.excludePathPatterns("/**/*.css","/**/*.js","/**/*.jpg","/**/*.png","/**/*.jpeg");
}
}
3.添加权限信息 CommunityConstant
public interface CommunityConstant {
/**
* 激活成功
*/
int ACTIVATION_SUCCESS=0;
/**
*重复激活
*/
int ACTIVATION_REPEAT=1;
/**
* 激活失败
*/
int ACTIVATION_FAILURE=2;
/**
* 默认状态的登陆凭证超时时间
*/
int DEFAULT_EXPIRE_SECONDS=3600*12;//12小时
/**
* 记住状态的登陆凭证超时时间
*/
int REMMERBER_EXPIRED_SECONDS=3600*24*30;//一个月
/**
* 实体类型:帖子
*/
int ENTITY_TYPE_POST=1;
/**
* 实体类型:评论
*/
int ENTITY_TYPE_COMMENT=2;
/**
* 实体类型:用户
*/
int ENTITY_TYPE_USER=3;
/**
* 主题:评论
*/
String TOPIC_COMMENT="comment";
/**
* 主题:点赞
*/
String TOPIC_LIKE="like";
/**
* 主题:关注
*/
String TOPIC_FOLLOW="follow";
/**
* 主题:发帖
*/
String TOPIC_PUBLISH="publish";
/**
* 系统用户 id
*/
int SYSTEM_USER_ID=1;
/**
* 权限:普通用户
*/
String AUTHORITY_USER="user";
/**
* 权限:管理员
*/
String AUTHORITY_ADMIN="admin";
/**
* 权限:版主
*/
String AUTHORITY_MODERATOR="moderator";
}
4.SecurityConfig
import com.nowcoder.community.util.CommunityConstant;
import com.nowcoder.community.util.CommunityUtil;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter implements CommunityConstant {
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/resources/**");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//授权
http.authorizeRequests()
.antMatchers(
"/comment/add/**",
"/discuss/add",
"/follow",
"/unfollow",
"/like",
"/letter/**",
"/notice/**",
"/setting",
"/upload",
"/updatePassword"
)
.hasAnyAuthority(
AUTHORITY_USER,AUTHORITY_ADMIN,AUTHORITY_MODERATOR
)
.anyRequest().permitAll()
.and().csrf().disable();//禁用csrf
//权限不够时的处理
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");//此时,Security会拦截securitylogout请求
}
}
5.在UserService中添加获取用户权限的方法
import com.nowcoder.community.entity.LoginTicket;
import com.nowcoder.community.entity.User;
import com.nowcoder.community.mapper.UserMapper;
import com.nowcoder.community.util.CommunityConstant;
import com.nowcoder.community.util.CommunityUtil;
import com.nowcoder.community.util.MailClient;
import com.nowcoder.community.util.RedisKeyUtil;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Service;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;
import java.util.*;
import java.util.concurrent.TimeUnit;
@Service
public class UserServiceImpl implements UserService, CommunityConstant {
@Autowired
private UserMapper userMapper;
@Autowired
private MailClient mailClient;
@Autowired
private TemplateEngine templateEngine;
@Autowired
@Qualifier("redisTemplates")
private RedisTemplate redisTemplate;
/**
* @Autowired
* private LoginTickerMapper loginTickerMapper;
*/
@Value("${community.path.domain}")
private String domain;
@Value("${server.servlet.context-path}")
private String contextPath;
public User selectById(int id) {
//return userMapper.selectById(id);
User user = getCache(id);
if (user==null){
user = initCache(id);
}
return user;
}
@Override
public User selectByName(String username) {
return userMapper.selectByName(username);
}
@Override
public User selectByEmail(String email) {
return userMapper.selectByEmail(email);
}
@Override
public int insertUser(User user) {
return userMapper.insertUser(user);
}
@Override
public int updateStatus(int id, int status) {
return userMapper.updateStatus(id, status);
}
@Override
public int updateHeader(int id, String headerUrl) {
//return userMapper.updateHeader(id, headerUrl);
int rows = userMapper.updateHeader(id, headerUrl);
clearCache(id);
return rows;
}
@Override
public int updatePassword(int id, String password) {
//return userMapper.updatePassword(id, password);
int rows = userMapper.updatePassword(id, password);
clearCache(id);
return rows;
}
/**
* 注册账号
* @param user
* @return
*/
public Map<String, Object> register(User user) {
Map<String, Object> map = new HashMap<>();
// 空值处理
if (user == null) {
throw new IllegalArgumentException("参数不能为空!");
}
if (StringUtils.isBlank(user.getUsername())) {
map.put("usernameMsg", "账号不能为空!");
return map;
}
if (StringUtils.isBlank(user.getPassword())) {
map.put("passwordMsg", "密码不能为空!");
return map;
}
if (StringUtils.isBlank(user.getEmail())) {
map.put("emailMsg", "邮箱不能为空!");
return map;
}
// 验证账号
User u = userMapper.selectByName(user.getUsername());
if (u != null) {
map.put("usernameMsg", "该账号已存在!");
return map;
}
// 验证邮箱
u = userMapper.selectByEmail(user.getEmail());
if (u != null) {
map.put("emailMsg", "该邮箱已被注册!");
return map;
}
// 注册用户
user.setSalt(CommunityUtil.generateUUID().substring(0, 5));
user.setPassword(CommunityUtil.md5(user.getPassword() + user.getSalt()));
user.setType(0);
user.setStatus(0);
user.setActivationCode(CommunityUtil.generateUUID());
user.setHeaderUrl(String.format("http://images.nowcoder.com/head/%dt.png", new Random().nextInt(1000)));
user.setCreateTime(new Date());
userMapper.insertUser(user);
// 激活邮件
Context context = new Context();
context.setVariable("email", user.getEmail());
// http://localhost:8080/community/activation/101/code
String url = domain + contextPath + "/activation/" + user.getId() + "/" + user.getActivationCode();
context.setVariable("url", url);
String content = templateEngine.process("/mail/activation", context);
mailClient.sendMail(user.getEmail(), "激活账号", content);
return map;
}
/**
* 激活账号
* @param userId
* @param code
* @return
*/
public int activation(int userId,String code){
User user = userMapper.selectById(userId);
if (user.getStatus()==1){
return ACTIVATION_REPEAT;
}
else if (user.getActivationCode().equals(code)){
userMapper.updateStatus(userId,1);
clearCache(userId);
return ACTIVATION_SUCCESS;
}
else{
return ACTIVATION_FAILURE;
}
}
/**
* 登录功能
* @param username
* @param password
* @param expiredSeconds
* @return
*/
public Map<String,Object> login(String username,String password,Long expiredSeconds){
Map<String,Object> map=new HashMap<>();
//空值处理
if (StringUtils.isBlank(username)){
map.put("usernameMsg","账号不能为空!");
return map;
}
if (StringUtils.isBlank(password)){
map.put("passwordMsg","密码不能为空!");
return map;
}
//验证账号
User user = userMapper.selectByName(username);
if (user==null){
map.put("usernameMsg","该账号不存在!");
return map;
}
//验证状态
if (user.getStatus()==0){
map.put("usernameMsg","该账号未激活!");
return map;
}
//验证密码
password=CommunityUtil.md5(password+user.getSalt());
if (!user.getPassword().equals(password)){
map.put("passwordMsg","密码不正确!");
return map;
}
//生成登陆凭证
LoginTicket loginTicket = new LoginTicket();
loginTicket.setUserId(user.getId());
loginTicket.setTicket(CommunityUtil.generateUUID());
loginTicket.setStatus(0);
loginTicket.setExpired(new Date(System.currentTimeMillis()+expiredSeconds*1000L));
//loginTickerMapper.insertLoginTicket(loginTicket);
String ticketKey = RedisKeyUtil.getTicketKey(loginTicket.getTicket());
redisTemplate.opsForValue().set(ticketKey,loginTicket);
map.put("ticket",loginTicket.getTicket());
return map;
}
/**
* 退出登录
* @param ticket
*/
public void logout(String ticket){
//loginTickerMapper.updateStatus(ticket,1);
String ticketKey = RedisKeyUtil.getTicketKey(ticket);
LoginTicket loginTicket = (LoginTicket) redisTemplate.opsForValue().get(ticketKey);
loginTicket.setStatus(1);
redisTemplate.opsForValue().set(ticketKey,loginTicket);
}
//1.优先从缓存中取值
private User getCache(int userId){
String userKey = RedisKeyUtil.getUserKey(userId);
User user = (User) redisTemplate.opsForValue().get(userKey);
return user;
}
//2.取不到时初始化缓存
private User initCache(int userId){
User user = userMapper.selectById(userId);
String userKey = RedisKeyUtil.getUserKey(userId);
redisTemplate.opsForValue().set(userKey,user,3600, TimeUnit.SECONDS);
return user;
}
//3.数据变更时清除缓存
private void clearCache(int userId){
String userKey = RedisKeyUtil.getUserKey(userId);
redisTemplate.delete(userKey);
}
public Collection<? extends GrantedAuthority> getAuthorities(int userId){
User user = this.selectById(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;
}
}
6.LoginTicketInterceptor
因为,SpringSecurity自己可以实现认证和权限,但是由于我们已经在系统中构建了认证的逻辑。因此,我们需要绕过SpringSecurity的认证,只利用权限。我们需要自己获取用户认证的结果,将认证结果存入到SecurityContext中,这是因为Security进行授权需要获取相关内容。
import com.nowcoder.community.entity.LoginTicket;
import com.nowcoder.community.entity.User;
import com.nowcoder.community.service.LoginTicketServiceImpl;
import com.nowcoder.community.service.UserServiceImpl;
import com.nowcoder.community.util.CookieUtil;
import com.nowcoder.community.util.HostHolder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextImpl;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Date;
@Component
public class LoginTicketInterceptor implements HandlerInterceptor {
@Autowired
private HostHolder hostHolder;
@Autowired
private LoginTicketServiceImpl loginTicketService;
@Autowired
private UserServiceImpl userService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//从Cookie中获取凭证
String ticket = CookieUtil.getValue(request, "ticket");
if (ticket!=null){
//查询凭证
LoginTicket loginTicket = loginTicketService.selectByTicket(ticket);
//查询凭证是否有效
if (loginTicket!=null&&loginTicket.getStatus()==0&&loginTicket.getExpired().after(new Date())){
//根据凭证查询用户
User user = userService.selectById(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 {
User user = hostHolder.getUser();
if (user!=null&&modelAndView!=null){
modelAndView.addObject("loginUser",user);
}
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
hostHolder.clear();
SecurityContextHolder.clearContext();
}
}
7.LoginController的退出方法中将用户认证的结果清除
@GetMapping("/logout")
public String logout(@CookieValue("ticket") String ticket){
userService.logout(ticket);
SecurityContextHolder.clearContext();
return "redirect:/login";
}
8.CSRF配置
防止CSRF攻击的基本原理,以及表单、AJAX的相关配置。
---CSRF攻击:某网站盗取你的Cookie(ticket)凭证,模拟你的身份访问服务器。(发生在提交表单的时候)。
---首先浏览器已经与服务器建立连接,服务器已经将登录凭证存在浏览器的cookie中。
---当浏览器再次向服务器发起请求,一般是获取表单;服务器正常响应表单至浏览器。
---但是某网站盗取浏览器的Cookie(ticket)凭证,模拟你的身份访问服务器,提交表单,造成CSRF攻击。
解决:
Security会在表单里增加一个TOCKEN(自动生成),恶意网站虽然可以获取cookie但无法获取TOCKEN,因此保证了安全。
但是异步请求Security无法在html文件生成CSRF令牌(异步不是通过请求体传数据,通过请求头)。
发送AJAX请求之前,将CSRF令牌设置到请求的消息头中。
index.html
<!--访问该页时,在此处生成CSRF令牌-->
<!--<meta name="_csrf" th:content="${_csrf.token}">-->
<!--<meta name="_csrf_header" th:content="${_csrf.headerName}">-->
index.js
$(function(){
$("#publishBtn").click(publish);
});
function publish() {
$("#publishModal").modal("hide");
//发送AJAX请求之前,将CSRF令牌设置到请求的消息头中.
// 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);
// });
//获取标题内容
var title=$("#recipient-name").val();
var content=$("#message-text").val();
//发送异步请求
$.post(
CONTEXT_PATH+"/discuss/add",
{"title":title,"content":content},
function (data) {
data=$.parseJSON(data);
//在提示框中显示返回消息
$("#hintBody").text(data.msg);
//显示提示框
$("#hintModal").modal("show");
//2秒后,自动隐藏提示框
setTimeout(function(){
$("#hintModal").modal("hide");
//刷新页面
if (data.code==0){
window.location.reload();
}
}, 2000);
}
);
}
可以在config配置取消CSRF