文章目錄
- 專案總結
- 后端基礎配置
- 1、MybatisPlus
- 2、JWT
- 3、Redis
- 4、ThreadLocal
- 5、攔截器
- 6、執行緒池
- 7、AOP
- 日志
- 快取
- 8、七牛云
- 9、SpringSecurity
專案總結
SpringBoot+MybatisPlus+Redis+Vue+SpringSecurity 前后端分離個人博客
- 采用
前后端分離,前端提供介面,后端根據介面開發,加上后臺管理系統 - 采用
MybatisPlus優化簡化sql,簡化代碼 - 采用
JWT來存盤用戶資訊,將token放到Redis中,防止過多session對服務端造成性能問題,將token放到請求頭中,下次請求需要用戶資訊的介面直接訪問redis中,避免與資料庫過多互動,并且采用ThreadLocal保存用戶資訊,在登陸成功后存入用戶,使執行緒全域私有用戶資訊,比如寫文章的時候直接從ThreadLocal中拿取用戶資訊,并且要及時移除用戶資訊,避免記憶體泄露, - 采用
攔截器,攔截需要登陸訪問的介面 - 采用
執行緒池,更新閱讀次數,主要是防止更新阻塞其他的讀操作,因為更新操作有鎖,性能就會比較低,所以更新閱讀數扔到執行緒池中去執行,這樣不會影響主執行緒的操作了, - 采用
AOP實作快取和日志功能,在介面上加上快取減少與資料庫的互動,在介面上加上日志,在我們排錯的時候可以快速定位 - 采用
七牛云來存放我們的靜態資源,加快博客的訪問速度,降低我們自身應用服務器的帶寬消耗 - 采用
SpringSecurity來實作后臺管理系統的認證和授權,來對用戶的統一管理
后端基礎配置
1、MybatisPlus
配置
spring.datasource.url=jdbc:mysql://localhost:3306/blog?useUnicode=true&characterEncoding=UTF-8&serverTimeZone=UTC
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
# 與資料庫操作的時候自動在表前面加上ms_
mybatis-plus.global-config.db-config.table-prefix=ms_
@Configuration
//掃包,將此包下的介面生成代理實作類,并且注冊到spring容器中
@MapperScan("com.liu.blog.dao.mapper")
public class MybatisPlusConfig {
//分頁插件
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return interceptor;
}
}
-
當我們使用MybatisPlus的插入方法,如果資料庫采用的自增id,那么需要在物體id加上
@TableId(value ="id", type = IdType.AUTO),不然他會按照mybatis-plus方式設定的主鍵 -
可以使用LambdaQueryWrapper條件構造器來替換QueryWrapper,前者可以跟好的使用lambda方法傳參
-
LambdaQueryWrapper<Tag> tagLambdaQueryWrapper = new LambdaQueryWrapper<>(); tagLambdaQueryWrapper.eq(Tag::getId,id); tagLambdaQueryWrapper.last("limit "+1); // 表示在最后加上 limit 1; ====================================================== QueryWrapper<Tag> tagQueryWrapper = new QueryWrapper<>(); tagQueryWrapper.eq("id",id); tagQueryWrapper.last("limit "+1); -
多表查詢需要自定義sql
select id ,tag_name from ms_tag where id in
(select tag_id from
(select tag_id from ms_article_tag group by tag_id order by count(tag_id) desc limit 2) as aliasA
)
# 這里as aliasA 和select tag_id from 是關鍵,相當于把查詢結果當作一個新表去查詢,
# 當然多表查詢也可以采用map映射
- 按多個屬性遞減
select FROM_UNIXTIME(create_date/1000,'%Y') as year,FROM_UNIXTIME(create_date/1000,'%m') as month,count(*) as count from ms_article group by year,month order by year desc,month desc
# order by year desc,month desc
-
賦值兩個物件,
BeanUtils.copyProperties(article,articleVo),將article與articleVo屬性型別相同的復制給articleVo不同的屬性型別,比如vo中id是String,則要手動articleVo.setId(String.valueOf(article.getId()))
String.valueOf(article.getId()) // 可以避免空指標例外,article.getId().toString不能 -
當引數時集合的時候
# 根據tagIds陣列中的id回傳其id對應的List<Tag> <select id="findTagsByTagIds" parameterType="list" resultType="com.liu.blog.dao.pojo.Tag"> select id,tag_name as tagName from ms_tag where id in <foreach collection="tagIds" item="tagId" separator="," open="(" close=")"> #{tagId} </foreach> </select>
2、JWT
登陸成功創建token,存放我們的用戶資訊,替換session,避免過多session對服務器造成壓力
public class JWTUtils {
// 設定秘鑰,用于加密解密
private static final String jwtToken = "123456liu!@#$$";
// 根據用戶id創建token
public static String createToken(Long userId){
// 將用戶id以map的形式封裝,取的時候好取
Map<String,Object> claims = new HashMap<>();
claims.put("userId",userId);
JwtBuilder jwtBuilder = Jwts.builder()
.signWith(SignatureAlgorithm.HS256, jwtToken) // 簽發演算法,秘鑰為jwtToken
.setClaims(claims) // body資料,要唯一,自行設定
.setIssuedAt(new Date()) // 設定簽發時間
.setExpiration(new Date(System.currentTimeMillis() + 24 * 60 * 60 * 60 * 1000));// 一天的有效時間
String token = jwtBuilder.compact();// 將所有的拼接成最終的token
return token;
}
public static Map<String, Object> checkToken(String token){
try {
Jwt parse = Jwts.parser().setSigningKey(jwtToken).parse(token);
return (Map<String, Object>) parse.getBody();
}catch (Exception e){
e.printStackTrace();
}
return null;
}
// 檢測token是否好使
// @Test
public void parse(){
String token = createToken(12l);
Jwt parse = Jwts.parser().setSigningKey(jwtToken).parse(token);
Map<String, Object> body = (Map<String, Object>) parse.getBody();
System.out.println(body.get("userId"));
System.out.println(parse);
/**
* 12
* header={alg=HS256},body={exp=1639296423, userId=12, iat=1638407390},
* signature=MYtDVcNccgWgEHROEG3nLW1jLfQzyWjCdiLtW4UnDs4
*/
}
}
使用
String token = JWTUtils.createToken(sysUser.getId());
3、Redis
登陸或注冊成功,存放我們的token和對應的用戶到redis中,這樣下次請求獲取當前用戶的介面的時候傳入token,根據token去redis中查詢,減少與資料庫的互動,比如攔截器中傳入token判斷是否有用戶資訊,這時候就直接去redis中判斷,并且創建用戶、創建token、存放redis時原子操作,所以可以加事務在類上加:@Transactional 注解即可
配置
# redis的介面配置
spring.redis.host=localhost
spring.redis.port=6379
// 這里我們可以將token和user存到redis中,下次再請求token的時候,可以直接從redis中獲取user,不用再決議token然后再去資料庫中取
redisTemplate.opsForValue().set("TOKEN_"+token, JSON.toJSONString(sysUser),1, TimeUnit.DAYS);
退出登陸的時候移除即可
redisTemplate.delete("TOKEN_"+token);
4、ThreadLocal
存放我們的用戶,執行緒隔離,比如當我們寫文章,先要被攔截,然后登陸后就會將user放到ThreadLocal中,使此次執行緒全域共享這個user,比如寫好文章將文章引數傳到后端此時就可以直接去ThreadLocal中拿到user來創建文章,
/**
* @author ljy
* @version 1.0.0
* @ClassName 全域的user資訊
* @Description TODO
* @createTime 2021年12月02日 13:46:00
*/
public class UserThreadLocal {
private UserThreadLocal(){}
//執行緒變數隔離
private static final ThreadLocal<SysUser> LOCAL = new ThreadLocal<>();
public static void put(SysUser sysUser){
LOCAL.set(sysUser);
}
public static SysUser get(){
return LOCAL.get();
}
public static void remove(){
LOCAL.remove();
}
}
UserThreadLocal.put(sysUser);
5、攔截器
主要是為了攔截一些需要登陸過后才能操作的介面,比如寫文章,評論等
配置
@Configuration
public class WebMVCConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addCorsMappings(CorsRegistry registry) {
//跨域配置,前后端分離埠不同,所以要配置
registry.addMapping("/**").allowedOrigins("http://localhost:8080");
// registry.addMapping("/**")
// .allowedOriginPatterns("*")
// .allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
// .allowCredentials(true)
// .maxAge(3600)
// .allowedHeaders("*");
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
//攔截test介面,后續實際遇到需要攔截的介面時,再配置為真正的攔截介面
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/test")
.addPathPatterns("/comments/create/change") //評論前要先登錄
.addPathPatterns("/articles/publish"); // 寫文章前也要登錄
}
}
使用:自動執行,當訪問我們攔截的介面
@Component
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
@Autowired
private LoginService loginService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//在執行controller方法(Handler)之前進行執行
/**
* 1. 需要判斷 請求的介面路徑 是否為 HandlerMethod (controller方法)
* 2. 判斷 token是否為空,如果為空 未登錄
* 3. 如果token 不為空,登錄驗證 loginService checkToken
* 4. 如果認證成功 放行即可
*/
if (!(handler instanceof HandlerMethod)){
//handler 可能是 RequestResourceHandler springboot 程式 訪問靜態資源 默認去classpath下的static目錄去查詢
return true;
}
String token = request.getHeader("Authorization");
log.info("=================request start===========================");
String requestURI = request.getRequestURI();
log.info("request uri:{}",requestURI);
log.info("request method:{}",request.getMethod());
log.info("token:{}", token);
log.info("=================request end===========================");
if (StringUtils.isBlank(token)){
Result result = Result.fail(ErrorCode.NO_LOGIN.getCode(), "未登錄");
response.setContentType("application/json;charset=utf-8");
response.getWriter().print(JSON.toJSONString(result));
return false;
}
SysUser sysUser = loginService.checkToken(token);
if (sysUser == null){
Result result = Result.fail(ErrorCode.NO_LOGIN.getCode(), "未登錄");
response.setContentType("application/json;charset=utf-8");
response.getWriter().print(JSON.toJSONString(result));
return false;
}
//登錄驗證成功,放行
//我希望在controller中 直接獲取用戶的資訊 怎么獲取?
UserThreadLocal.put(sysUser);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//如果不洗掉 ThreadLocal中用完的資訊 會有記憶體泄漏的風險
UserThreadLocal.remove();
}
}
6、執行緒池
當我們查詢某一篇文章時,閱讀數要相應增加,但是更新操作會加鎖阻塞讀操作,這樣就會影響文章詳情的回應,所以把更新操作交給執行緒池來做,這樣就不會影響主執行緒的查詢文章詳情
/**
* @author ljy1999
* @version 1.0.0
* @ClassName 執行緒池來更新閱讀次數
* @Description TODO
* @createTime 2021年12月02日 14:50:00
*/
@Configuration
@EnableAsync
public class ThreadPoolConfig {
@Bean("taskExecutor")
public Executor asyncServiceExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 設定核心執行緒數
executor.setCorePoolSize(5);
// 設定最大執行緒數
executor.setMaxPoolSize(20);
//配置佇列大小
executor.setQueueCapacity(Integer.MAX_VALUE);
// 設定執行緒活躍時間(秒)
executor.setKeepAliveSeconds(60);
// 設定默認執行緒名稱
executor.setThreadNamePrefix("小劉博客專案");
// 等待所有任務結束后再關閉執行緒池
executor.setWaitForTasksToCompleteOnShutdown(true);
//執行初始化
executor.initialize();
return executor;
}
}
/**
* @author ljy1999
* @version 1.0.0
* @ClassName ThreadService.java
* @Description TODO
* @createTime 2021年12月02日 14:51:00
*/
@Service
public class ThreadService {
//期望此操作在執行緒池 執行 不會影響原有的主執行緒
@Async("taskExecutor")
public void updateArticleViewCount(ArticleMapper articleMapper, Article article){
// 修改文章
Article articleUpdate = new Article();
// 閱讀加一
articleUpdate.setViewCounts(article.getViewCounts() + 1);
// 使用LambdaQueryWrapper,在eq中就可以直接用Article::getId形式,而不用去知道資料庫中的欄位是什么
LambdaQueryWrapper<Article> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Article::getId,article.getId());
//設定一個ViewCounts 為了在多執行緒的環境下 執行緒安全
queryWrapper.eq(Article::getViewCounts,article.getViewCounts());
// 這個時候文章閱讀已經被修改 update article set view_count=100 where view_count=99 and id=11
articleMapper.update(articleUpdate,queryWrapper);
/* try {
//睡眠5秒 證明不會影響主執行緒的使用
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}*/
}
}
//查看完文章了,新增閱讀數,有沒有問題呢?
//查看完文章之后,本應該直接回傳資料了,這時候做了一個更新操作,更新時加寫鎖,阻塞其他的讀操作,性能就會比較低
// 更新 增加了此次介面的 耗時 如果一旦更新出問題,不能影響 查看文章的操作
//執行緒池 可以把更新操作 扔到執行緒池中去執行,和主執行緒就不相關了
threadService.updateArticleViewCount(articleMapper,article);
7、AOP
日志
創建注解
/**
* @author ljy
* @version 1.0.0
* @Description 日志注解
* @createTime 2021年12月02日 22:39:00
*/
//Type 代表可以放在類上面 Method 代表可以放在方法上
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented // 這三個注解是固定的
public @interface LogAnnotation {
// 模塊名稱 默認為空
String module() default "";
// 操作名稱
String operator() default "";
}
配置注解
/**
* @author ljy
* @version 1.0.0
* @Description 日志切面
* @createTime 2021年12月02日 22:40:00
*/
@Aspect //切面 定義了通知和切點的關系
@Component
@Slf4j // 記錄日志
public class LogAspect {
// 定義切點 :com.liu.blog.common.aop.LogAnnotation
// 切點是這個注解 就表示這個注解加到哪 哪就是切點
@Pointcut("@annotation(com.liu.blog.common.aop.LogAnnotation)")
public void logPointCut() {
}
// 通知類 標識切點logPointCut
// 環繞通知
@Around("logPointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
long beginTime = System.currentTimeMillis(); // 記錄開始時間
Object result = point.proceed(); //執行原有方法
//執行時長(毫秒)
long time = System.currentTimeMillis() - beginTime;
//保存日志
recordLog(point, time);
return result;
}
// 記錄日志
private void recordLog(ProceedingJoinPoint joinPoint, long time) {
// 拿到我們的方法,拿到對應的LogAnnotation注解
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
LogAnnotation logAnnotation = method.getAnnotation(LogAnnotation.class);
log.info("=====================log start================================");
log.info("module:{}",logAnnotation.module());
log.info("operation:{}",logAnnotation.operator());
// 請求的方法名
String className = joinPoint.getTarget().getClass().getName();
String methodName = signature.getName();
log.info("request method:{}",className + "." + methodName + "()");
// 請求的引數
Object[] args = joinPoint.getArgs();
String params = JSON.toJSONString(args[0]);
log.info("params:{}",params);
// 獲取request 設定IP地址
HttpServletRequest request = HttpContextUtils.getHttpServletRequest();
log.info("ip:{}", IpUtils.getIpAddr(request));
// 記錄執行時間
log.info("excute time : {} ms",time);
log.info("=====================log end================================");
}
}
使用
/**
* 首頁 文章串列
* @param pageParams
* @return
*/
@PostMapping
// 加上此注解 代表要對此介面記錄日志
@LogAnnotation(module="文章",operator="獲取文章串列")
public Result listArticle(@RequestBody PageParams pageParams){
return articleService.listArticle(pageParams);
}

快取
創建注解
/**
* @author ljy
* @version 1.0.0
* @Description TODO
* @createTime 2021年12月03日 10:54:00
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Cache {
// 過期時間
long expire() default 1 * 60 * 1000;
// 快取標識 key
String name() default "";
}
配置注解
/**
* @author ljy
* @version 1.0.0
* @Description TODO
* @createTime 2021年12月03日 10:55:00
*/
@Aspect // aop 定義一個切面,切面定義了切點和通知的關系
@Component
@Slf4j
public class CacheAspect {
@Autowired
private RedisTemplate<String, String> redisTemplate;
// 切點
@Pointcut("@annotation(com.liu.blog.common.cache.Cache)")
public void pt(){}
// 通知
@Around("pt()")
public Object around(ProceedingJoinPoint pjp){
try {
Signature signature = pjp.getSignature();
//類名
String className = pjp.getTarget().getClass().getSimpleName();
//呼叫的方法名
String methodName = signature.getName();
// 引數型別
Class[] parameterTypes = new Class[pjp.getArgs().length];
// 請求的引數
Object[] args = pjp.getArgs();
//引數
String params = "";
for(int i=0; i<args.length; i++) {
if(args[i] != null) {
params += JSON.toJSONString(args[i]);
parameterTypes[i] = args[i].getClass();
}else {
parameterTypes[i] = null;
}
}
if (StringUtils.isNotEmpty(params)) {
//加密 以防出現key過長以及字符轉義獲取不到的情況
params = DigestUtils.md5Hex(params);
}
// 主要是為了拿到注解,拿到方法再去拿到注解
Method method = pjp.getSignature().getDeclaringType().getMethod(methodName, parameterTypes);
//獲取Cache注解
Cache annotation = method.getAnnotation(Cache.class);
//快取過期時間
long expire = annotation.expire();
//快取名稱
String name = annotation.name();
//先從redis獲取 redisKey:注解名稱+類名+方法名稱+md5引數
String redisKey = name + "::" + className+"::"+methodName+"::"+params;
// 先去快取中那,如果有就走快取
String redisValue = redisTemplate.opsForValue().get(redisKey);
if (StringUtils.isNotEmpty(redisValue)){
log.info("走了快取~~~,{},{}",className,methodName);
return JSON.parseObject(redisValue, Result.class);
}
// 呼叫我們的方法
Object proceed = pjp.proceed();
// 將方法回傳的結果轉為json 存到redis中
redisTemplate.opsForValue().set(redisKey,JSON.toJSONString(proceed), Duration.ofMillis(expire));
log.info("存入快取~~~ {},{}",className,methodName);
return proceed;
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return Result.fail(-999,"系統錯誤");
}
}
使用快取
/**
* 首頁 文章串列
* @param pageParams
* @return
*/
@PostMapping
// 加上此注解 代表要對此介面記錄日志
@LogAnnotation(module="文章",operator="獲取文章串列")
// 快取 五分鐘
@Cache(expire = 5 * 60 * 1000,name = "listArticle")
public Result listArticle(@RequestBody PageParams pageParams){
return articleService.listArticle(pageParams);
}
第一次先存入快取

第二次直接從快取中取,可以看到明顯加快訪問速度,主要避免短時間大量介面請求

8、七牛云
配置
# 七牛云的秘鑰 在七牛云官網找
qiniu.accessKey=???
qiniu.accessSecretKey=???
# 上傳檔案總的最大值
spring.servlet.multipart.max-request-size=20MB
# 單個檔案的最大值
spring.servlet.multipart.max-file-size=2MB
/**
* @author ljy
* @version 1.0.0
* @Description 七牛云
* @createTime 2021年12月03日 21:10:00
*/
@Component
public class QiniuUtils {
// 七牛云30使用域名
public static final String url = "http://r3jkcbpns.hb-bkt.clouddn.com/";
@Value("${qiniu.accessKey}")
private String accessKey;
@Value("${qiniu.accessSecretKey}")
private String accessSecretKey;
public boolean upload(MultipartFile file, String fileName){
//構造一個帶指定 Region 物件的配置類
Configuration cfg = new Configuration(Region.huabei());
//...其他引數參考類注釋
UploadManager uploadManager = new UploadManager(cfg);
//...生成上傳憑證,然后準備上傳,這個是七牛云官網的空間名稱
String bucket = "liujiany";
//默認不指定key的情況下,以檔案內容的hash值作為檔案名
try {
byte[] uploadBytes = file.getBytes();
Auth auth = Auth.create(accessKey, accessSecretKey);
String upToken = auth.uploadToken(bucket);
Response response = uploadManager.put(uploadBytes, fileName, upToken);
//決議上傳成功的結果
DefaultPutRet putRet = JSON.parseObject(response.bodyString(), DefaultPutRet.class);
return true;
} catch (Exception ex) {
ex.printStackTrace();
}
return false;
}
}
使用
/**
* @author ljy
* @version 1.0.0
* @Description 上傳
* @createTime 2021年12月03日 10:12:00
*/
@RestController
@RequestMapping("upload")
public class UploadController {
@Autowired
private QiniuUtils qiniuUtils;
@PostMapping
// RequestParam前端傳入的引數名稱,MultipartFile是SpringBoot用于接受檔案的一個型別
public Result upload(@RequestParam("image") MultipartFile file){
// file.getOriginalFilename() 原始檔案名稱 比如 傳入aa.png,拿到的就是aa.png
// 上傳的名稱應該是隨機的不能是aa.png
// StringUtils.substringAfterLast(file.getOriginalFilename(), ".")的作用就是拿到png
String fileName = UUID.randomUUID().toString() + "." + StringUtils.substringAfterLast(file.getOriginalFilename(), ".");
// 可以上傳到服務器,但是不建議,采用七牛云來管理
// 七牛云 云服務器 按量付費 速度快 把圖片發放到離用戶最近的服務器上
// 降低 我們自身應用服務器的帶寬消耗
boolean upload = qiniuUtils.upload(file, fileName);
if (upload){
return Result.success(QiniuUtils.url + fileName);
}
return Result.fail(20001,"上傳失敗");
}
}
substringAfterLast原始碼


9、SpringSecurity
跟攔截器類似,但是SpringSecurity可以自定義權限,可以指定用戶擁有哪些權限
配置
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// springSecurity的加密策略bCrypt
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder(){
return new BCryptPasswordEncoder();
}
public static void main(String[] args) {
//加密策略 MD5 不安全 彩虹表 MD5 加鹽
String liu = new BCryptPasswordEncoder().encode("123456");
System.out.println(liu); // 將123456加密
// $2a$10$A.hEQYiBLyN6K5dBwfCGlenTcUIMflLwiR/eXa5ojCPR0Efj2LJpe
// $2a$10$RZECQ90DjOT/t1mhnXsl5.XSuZWc0Sa1XduPxj2rb4yaSYcje3nWW
}
@Override
public void configure(WebSecurity web) throws Exception {
super.configure(web);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests() //開啟登錄認證
// .antMatchers("/user/findAll").hasRole("admin") // 訪問介面需要admin的角色
.antMatchers("/css/**").permitAll() // permit:放行的意思,就是所有的人都可以訪問
.antMatchers("/img/**").permitAll()
.antMatchers("/js/**").permitAll()
.antMatchers("/plugins/**").permitAll()
// 表示admin下的路徑要通過認證,這樣就不影響login頁面的操作 自定義authService(里面實作auth) 來去實作實 時的權限認證,回傳true則通過
.antMatchers("/admin/**").access("@authService.auth(request,authentication)") // 自定義 service 來去實作實時的權限認證
.antMatchers("/pages/**").authenticated() // 登錄成功才能訪問
.and().formLogin()// 登錄配置
.loginPage("/login.html") // 自定義的登錄頁面
.loginProcessingUrl("/login") // 登錄處理介面,這是SpringSecurity給我們提供的介面,不需要我們自己寫
.usernameParameter("username") // 定義登錄時的用戶名的key 默認為username ,對應login.html中name為 username的屬性
.passwordParameter("password") // 定義登錄時的密碼key,默認是password
.defaultSuccessUrl("/pages/main.html") // 登錄成功就跳轉到/pages/main.html
.failureUrl("/login.html") // 失敗就繼續登錄
.permitAll() // 通過 不攔截,更加前面配的路徑決定,這是指和登錄表單相關的介面 都通過
.and().logout() // 退出登錄配置
.logoutUrl("/logout") //退出登錄介面,也是默認提供的
.logoutSuccessUrl("/login.html")
.permitAll() // 退出登錄的介面放行
.and()
.httpBasic() // 單純的http訪問也攔截,比如 PostMan
.and()
.csrf().disable() // csrf關閉 如果自定義登錄 需要關閉,跨站請求偽造,默認只能post方式提交logout請求
.headers().frameOptions().sameOrigin();// 支持iframe頁面嵌套
}
}
authService.auth需要我們自己定義
@Service
public class AuthService {
@Autowired
private AdminService adminService;
public boolean auth(HttpServletRequest request, Authentication authentication){
// 權限認證
// 請求路徑
String requestURI = request.getRequestURI();
// 拿到當前用戶的資訊
Object principal = authentication.getPrincipal();
// 沒有登錄 或者是匿名用戶
if (principal == null || "anonymousUser".equals(principal)){
//未登錄
return false;
}
// 拿到用戶name
UserDetails userDetails = (UserDetails) principal;
String username = userDetails.getUsername();
// 再去資料庫的拿到用戶資訊
Admin admin = adminService.findAdminByUsername(username);
if (admin == null){
return false;
}
if (1 == admin.getId()){
//超級管理員
return true;
}
Long id = admin.getId();
List<Permission> permissionList = this.adminService.findPermissionByAdminId(id);
// 請求的路徑有可能有?傳參,所以我們要保證requestURI是?前面的路徑
requestURI = StringUtils.split(requestURI,'?')[0];
for (Permission permission : permissionList) {
// 如果用戶的訪問路徑和權限路徑相同則通過
if (requestURI.equals(permission.getPath())){
return true;
}
}
return false;
}
}
使用
// UserDetailsService 是SpringSecurity提供的
@Component
public class SecurityUserService implements UserDetailsService {
@Autowired
private AdminService adminService;
/**
* 登錄的時候,會自動把username 傳遞到這里
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 我們需要通過username查詢 admin表,如果 admin存在 將密碼告訴spring security
// 如果不存在 回傳null 認證失敗了
Admin admin = this.adminService.findAdminByUsername(username);
if (admin == null){
//登錄失敗
return null;
}
// 有此用戶但是還沒有驗證密碼,我們是通過SpringSecurity來加密的,所以將密碼交給SpringSecurity來幫我們驗證
UserDetails userDetails = new User(username,admin.getPassword(),new ArrayList<>());
return userDetails;
}
}
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/374754.html
標籤:java
