基于Springboot+redis+Springsecruity的网上商城
前言
最近复习了下SpringSecurity,发现看完之后还是和以前一样懵逼
既然理解不了,那我背下来可以把,就拿这玩意做一个小系统试试吧
项目地址在最后的总结里,放在gitee里了,有兴趣的话可以来拿
系统简介
整个系统就只有一个主要业务,就是买东西,其他都是围绕着这个内容展开的。(该系统并没有使用支付宝等平台的支付接口,有兴趣的可以加一下)
功能
整个系统的功能如下图所示
使用技术
模块 | 技术 |
---|---|
web框架 | Springboot |
安全框架 | SpringSecurity |
数据库框架 | mybatis-plus |
前端 | semanticUI,html(建议大家使用Vue重写下) |
主要功能
- 秒杀商品功能:这种场景下直接操作mysql数据库效率比较低,这里采用redis存储秒杀的商品,并使用乐观锁来实现商品货量的修改
- 购买商品功能:相对于秒杀,并发量较少,采用直接操作mysql数据库的方式
- 登录与授权:采用SpringSecurity作为安全框架,使用jwt方式存储账号信息,redis中存储着已登录用户的信息;添加一个自定义的过滤器,用于处理jwt中的账号信息,并进行拦截或放行以及授权。
- 上/下架商品
- 订单查看(用户/商家)
- 退货申请/退货/处理退货申请
- 评价(购买后)
具体实现
秒杀商品
我们先来分析下需求,首先就是一般秒杀都是在一个时间点然后才开始,开始后会同时涌入大量的用户进行购买。
首先是第一个问题,怎么设置一个时间点开启秒杀,这里本来使用的是Timer开启一个定时任务,但本着能用框架就用框架的心,这里使用的是Quartz来实现定时任务的操作。
这里简单描述下怎么使用Quartz,大致需要三个部分,任务,触发器和调度器,任务设置需要完成的事情,即操作redis中货品的信息,触发器设置开启时间,调度器根据触发器来调度任务。
以下是使用的一个小实例
LocalDateTime ctime = goods.getCtime();
int year = ctime.getYear();
int value = ctime.getMonth().getValue();
int dayOfMonth = ctime.getDayOfMonth();
int hour = ctime.getHour();
int minute = ctime.getMinute();
// CronTrigger设置时间的语法 秒 分 时 天 月 ? 年
String rtime = "0 " + minute + " " + hour + " " + dayOfMonth + " " + value + " ? " + year;
// 设置JobDetail 这里设置自己设置的Job类信息
JobDetail jobDetail = JobBuilder.newJob(MyJob.class)
.withIdentity("job" + LocalDateTime.now().toString(), "seckillgroup1")
//使用jobData传递信息到job中
.usingJobData("info", goods.getId().toString())
.usingJobData("size", goods.getSize().toString())
.build();
//设置调度器
CronTrigger trigger = (CronTrigger) TriggerBuilder.newTrigger()
.withIdentity("myTrigger" + LocalDateTime.now().toString(), "triggergroup1")
.withSchedule(
//设置任务开启时间
CronScheduleBuilder.cronSchedule(rtime)
).build();
try {
Scheduler defaultScheduler = StdSchedulerFactory.getDefaultScheduler();
//将任务和触发器添加进调度器中
defaultScheduler.scheduleJob(jobDetail, trigger);
defaultScheduler.start();
} catch (SchedulerException e) {
e.printStackTrace();
}
MyJob
package com.xiaow.springsecuriydemo.vo;
import com.xiaow.springsecuriydemo.utils.SpringContextUtil;
import org.apache.catalina.core.ApplicationContext;
import org.quartz.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import java.util.concurrent.TimeUnit;
/**
* 做定时器的任务 这里是做秒杀的业务
*
* @ClassName MyJob
* @Author xiaow
* @DATE 2022/9/18 19:07
**/
@DisallowConcurrentExecution //设置之后表示不允许多线程进行
@PersistJobDataAfterExecution //设置之后就不会创造新的Myjob实例
public class MyJob implements Job {
/**
* 秒杀商品的添加
* @param jobExecutionContext
* @throws JobExecutionException
*/
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
JobDetail jobDetail = jobExecutionContext.getJobDetail();
JobDataMap jobDataMap = jobDetail.getJobDataMap();
String info = (String) jobDataMap.get("info");
String size = (String) jobDataMap.get("size");
Integer num = Integer.parseInt(size);
RedisTemplate redisTemplate = (RedisTemplate)
SpringContextUtil.getApplicationContext().getBean("redisTemplate");
redisTemplate.opsForValue().set("seckill" + info, num, 60 * 60 * 24, TimeUnit.SECONDS);
}
}
接下来讲讲怎么购买,这个就相对简单些,也就是先判断redis中有无该商品的信息,如果有的话,开启乐观锁,然后进行商品数量减一操作,然后在操作成功后采取向数据库中添加购物信息
/**
* 秒杀商品购买
*
* @return
*/
@PostMapping("/buySeckill")
@PreAuthorize("hasAuthority('/user')")
// @Transactional
public Result buySeckill(@RequestBody Goods goods) {
System.out.println(goods);
Goods byId = goodsService.getById(goods.getId());
String key = "seckill" + goods.getId();
Object o1 = redisTemplate.opsForValue().get("seckill" + goods.getId());
if (Objects.isNull(o1))
return Result.fail("还未开始抢购");
//开启乐观锁
redisTemplate.watch("seckill" + goods.getId());
//开启多任务
redisTemplate.multi();
Integer remaining = (Integer) o1;
if (remaining <= 0) {
goodsService.updateById(byId.setState(3));
return Result.fail("抢完了");
}
redisTemplate.opsForValue().set("seckill" + goods.getId(), remaining - 1);
List exec = redisTemplate.exec();
// exec==null代表操作失败,即需要在操作一次
if (exec == null) {
return Result.fail("请稍后重试,抢购失败");
}
//使用SpringSecurity中的对象获取用户信息
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
byId.setRemaining(byId.getRemaining() - 1);
// 更新数据库中的库存量
boolean b = goodsService.updateById(byId);
// 加入订单
Trading trading = new Trading()
.setCtime(LocalDateTime.now())
.setGoodsid(goods.getId())
.setMoney(byId.getMoney())
.setStatus(0)
.setId(0)
.setUserid(loginUser.getUser().getId());
boolean save = tradingService.save(trading);
return Result.succ("抢购成功");
}
购买操作类似秒杀操作,只是不经过redis,直接操作数据库
接入SpringSeCruity
SecurityConfig
package com.xiaow.springsecuriydemo.config;
import com.xiaow.springsecuriydemo.exception.AuthenticationEntryPointImpl;
import com.xiaow.springsecuriydemo.filter.JwtAuthenticationTokenFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/**
* @ClassName Securityconfig
* @Author xiaow
* @DATE 2022/9/15 9:55
**/
@Configuration
//开启注解授权认证的注解
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
AccessDeniedHandler accessDeniedHandler;
@Autowired
AuthenticationEntryPoint authenticationEntryPoint;
@Autowired
private JwtAuthenticationTokenFilter authenticationTokenFilter;
// 重写密码加密器
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
public static void main(String[] args) {
String encode = new BCryptPasswordEncoder().encode("123");
System.out.println(encode);
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// 解决跨域问题
.cors().and()
// 关闭csrf
.csrf().disable()
// 禁用session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 放行接口 不需要认证
.antMatchers("/login", "/shoper/login", "/user/state", "/goods/getAllGoods"
, "/goods/getGoodsById", "/goods/getAllSeckillGoods", "/goods/getSeckillById", "/comments/getCommentsByGoodsId", "/goods/getAllByShopIdAll"
, "/shop/getShopInfoByShopId"
).permitAll()
.anyRequest().authenticated();
// 把我们自己写好的过滤器添加到过滤器的前面 这个很重要
http.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
// 配置异常处理器
// 分别是认证异常处理和授权处理
http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler);
}
}
设置自定义过滤器
package com.xiaow.springsecuriydemo.filter;
import com.xiaow.springsecuriydemo.entity.Roleandperm;
import com.xiaow.springsecuriydemo.entity.User;
import com.xiaow.springsecuriydemo.service.RoleandpermService;
import com.xiaow.springsecuriydemo.utils.JwtUtils;
import com.xiaow.springsecuriydemo.vo.LoginUser;
import io.jsonwebtoken.Claims;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
/**
* @ClassName JwtAuthenticationTokenFilter
* @Author xiaow
* @DATE 2022/9/15 11:03
**/
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
RoleandpermService roleandpermService;
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
//获取token 这里的token是由前端发起的请求中的header中存储的
String token = httpServletRequest.getHeader("token");
// System.out.println(token);
if (!StringUtils.hasText(token)) {
// 这里放行就是让其他的过滤器帮我们解决未登录
filterChain.doFilter(httpServletRequest, httpServletResponse);
// return是必须的,防止继续进行下面代码
return;
}
// 解析token
String userid = "";
try {
Claims claims = JwtUtils.getClaims(token);
userid = (String) claims.get("userid");
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("解析token异常");
}
// redis中获取信息
User o = (User) redisTemplate.opsForValue().get("login" + userid);
if (Objects.isNull(o)) {
throw new RuntimeException("token异常");
}
List<Roleandperm> byUserId = roleandpermService.getByUserId(o.getId());
List<GrantedAuthority> newList=new LinkedList<>();
List<String> perms=new LinkedList<>();
byUserId.forEach(p->{
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(p.getPerm());
newList.add(simpleGrantedAuthority);
perms.add(p.getPerm());
});
// 存入SecurityContextgholder,因为后续的过滤器需要在这个东西中找到认证的信息 这个很重要
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(new LoginUser(o,perms), null, newList);
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
}
登录异常和授权异常处理
处理权限不足时的异常处理器
设置这里,就是当权限不足时,可以以我们喜欢的方式提醒我们
package com.xiaow.springsecuriydemo.exception;
import com.alibaba.fastjson.JSON;
import com.xiaow.springsecuriydemo.utils.WebUtils;
import com.xiaow.springsecuriydemo.vo.Result;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @ClassName AccessDeniedHandlerImpl
* @Author xiaow
* @DATE 2022/9/16 19:28
**/
@Component
//处理权限不足时的异常处理器
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
//处理异常
Result result = Result.result("您的权限不足", 403, "");
String s = JSON.toJSONString(result);
WebUtils.renderString(httpServletResponse,s);
}
}
**登陆异常处理器 **
package com.xiaow.springsecuriydemo.exception;
import com.alibaba.fastjson.JSON;
import com.xiaow.springsecuriydemo.utils.WebUtils;
import com.xiaow.springsecuriydemo.vo.Result;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @ClassName AuthenticationEntryPointImpl
* @Author xiaow
* @DATE 2022/9/16 19:22
**/
//认证异常的处理
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
//处理异常
Result result = Result.result("用户名认证失败请重新登录", 401, "");
String s = JSON.toJSONString(result);
WebUtils.renderString(httpServletResponse,s);
}
}
这里还有很多配置没有提到,有兴趣的话可以去看看项目源码,都有
其他功能
放几张系统的图片
相对于秒杀来说,其他的业务都相对简单,也就是对数据库的增删查改,这里就不过多描述了。
这里的文件上传功能使用一个单独的服务来实现,有需要的可以来这里找一下文件服务代码,直接用
文件上传主要用于在商家上架商品和秒杀时使用
总结
有点标题党了,如果说毕设要求比较高的话,可以再加点东西。这里提几个改进的地方
- Quartz可以设置成持久化的,这个项目中使用的还差点意思
- 可以搞一个Redis集群
- 系统功能可以在划分一下,分成多个微服务
其他的大家就自己想了
项目地址项目源码