Vue+JWT+elementUI+Spring Security实现登录校验
前端vue
下载依赖
#安装cookies
npm install --save js-cookie
#安装axios
npm install --save axios vue-axios
#安装elementUI
npm i element-ui -S
#安装路由
#npm install vue-router@3
npm install vue-router@3 or 4
main.js
import Vue from 'vue'
import App from './App'
import router from "./router/index"
Vue.config.productionTip = false
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import axios from "axios";
import request from "./router/request"
Vue.prototype.$axios=axios;
Vue.use(ElementUI)
Vue.prototype.$request=request;
new Vue({
el: '#app',
router,
request,
components: { App },
template: '<App/>'
})
App.vue
<template>
<div id="app">
<router-view></router-view>
</div>
</template>
<script>
export default {
name: 'App',
components: {
}
}
</script>
<style>
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
router目录下index.js
import Vue from "vue"
import Router from "vue-router";
Vue.use(Router);
const router=new Router({
routes:[
{
name:"Login",
path:'/',
component: ()=>import("@/components/Login")
},
{
name:"Main",
path: "/Main",
component:()=>import("@/components/Main")
}
]
});
router.beforeEach((to, from,next) => {
console.log("to======>"+to.path);
console.log("from======>"+from.path);
const isLogin = true;
if(to.path!=="Login"){
if(isLogin){
next();
}
return false;
}
return next();
});
export default router;
request.js
import axios from "axios";
import cookie from "js-cookie"
const intance=axios.create({
baseURL : "http://localhost:9999",
timeout:10000
})
intance.interceptors.request.use(function (config) {
config.headers.token=cookie.get("token")
return config;
},function (error) {
console.log(error)
return Promise.reject(error)
})
intance.interceptors.response.use(function (response) {
const res=response.data
return res;
},function (error) {
return Promise.reject(error)
})
export default intance;
Login.vue
<template>
<div style="width: 300px">
<el-form ref="loginForm" :model="form" :rules="rules" label-width="80px" class="login-box">
<h3 class="login-title">欢迎登录</h3>
<el-form-item label="账号" prop="username">
<el-input type="text" placeholder="请输入账号" v-model="form.username"/>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input type="password" placeholder="请输入密码" v-model="form.password"/>
</el-form-item>
<el-form-item>
<el-button type="primary" v-on:click="onsubmit('loginForm')">登录</el-button>
</el-form-item>
</el-form>
<el-dialog title="温馨提示" width="30%">
<span>请输入账号和密码</span>
<span slot="footer" class="dialog-footer">
<el-button type="primary" @click="dialogVisible = false">确定</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
import cookie from "js-cookie"
export default {
name: "Login",
data(){
return{
form:{
username:'',
password:'',
},
//表单验证,需要在 el-form-item 元素中增加prop属性
rules:{
username:[
{required:true,message:"账号不可为空",trigger:"blur"}
],
password:[
{required:true,message:"密码不可为空",tigger:"blur"}
]
},
//对话框显示和隐藏
dialogVisible:false
}
},
methods:{
onsubmit(formName){
//为表单绑定验证功能
this.$refs[formName].validate((valid)=>{
if(valid){
this.$request.post("/user/doLogin?username="+this.form.username+"&password="+this.form.password)
.then((response)=>{
if (response!=="20001"){
// console.log(response.data.token)
// 第一个参数存储的token名,第二个参数是服务器返回的token值,第三个参数设置token的作用域
cookie.set("token",response.data.token,{admin:"localhost"})//设置token
console.log(cookie.get("token"))
this.$router.push('/Main');
}else {
alert(response.success)
alert("用户名或密码不正确")
}
})
}else{
this.dialogVisible=true;
return false;
}
});
}
}
}
</script>
<style scoped>
</style>
Main.vue
<template>
<div>
<h2>新闻列表</h2>
<el-row class="newssavebtn">
<el-button type="primary" @click="newsAdd()" icon="el-icon-plus" >新增</el-button>
</el-row>
<el-table
:data="newsList"
style="width: 100%">
<el-table-column
prop="title"
label="标题"
width="180">
</el-table-column>
<el-table-column
prop="publisher"
label="发布人">
</el-table-column>
<el-table-column
prop="createTime"
label="发布时间">
</el-table-column>
<el-table-column label="操作">
<!--template slot-scope="scope":存的是后台的数据-->
<template slot-scope="scope">
<!--scope.$index:获取索引,
scope.row:获取所有数据
scope.row.id:获取id,如果实现删除功能就写 @click="handleDelete(scope.row.id)
-->
<el-button
size="mini"
@click="handleEdit(scope.$index, scope.row)">编辑</el-button>
<el-button
size="mini"
type="danger"
@click="newsDelete(scope.row.id)">删除</el-button><!--scope.row.id:获取删除新闻id-->
</template>
</el-table-column>
</el-table>
<!--分页-->
<el-pagination
background
layout="prev, pager, next"
@current-change="handPage"
:page-size="pageSize"
:current-page="pageNum"
:total="total">
</el-pagination>
<!--新增和修改弹窗-->
<el-dialog
title="提示"
:visible.sync="dialogVisible"
width="30%">
<span>这是一段信息</span>
<el-form ref="form" :model="news" label-width="80px">
<el-form-item label="新闻标题">
<el-input v-model="news.title"></el-input>
</el-form-item>
<el-form-item label="新闻内容">
<el-input type="textarea" v-model="news.content"></el-input>
</el-form-item>
<el-form-item label="发布人">
<el-input v-model="news.publisher"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSubmit(news)" icon="el-icon-plus">提交</el-button>
<el-button>取消</el-button>
</el-form-item>
</el-form>
</el-dialog>
</div>
</template>
<script>
import cookie from "js-cookie"
export default {
name: "Main",
data(){
return{
newsList:[],
dialogVisible:false,//控制弹窗显示和隐藏
news:{},
pageSize:5,
pageNum:1,
total:0
}
},
methods:{
//分页查询
getNewsPage(){
this.$request.post("/news/list?pageNum="+this.pageNum+"&pageSize="+this.pageSize+"").then((response)=>{
console.log(response+"\t"+this.pageNum+"\t"+this.pageSize)
//将查询的数据放到newsList中
this.newsList=response.records
this.total=response.total;
})
.catch(function (error) {
console.log("=====>"+error)
})
},
//当前页面改变重新查数据 点击上一页,下一页
handPage(pageNum){
this.pageNum=pageNum
this.getNewsPage()
},
//处理新增
newsAdd(){
this.dialogVisible = true;
},
//新增新闻
newsAdds(){
this.$request.post('/news/add', this.news)
.then((response)=> {
if (response===true){
this.$message({
message: '新增成功',
type: 'success'
})
}
})
},
//查询要修改的数据,从前端的row中获取
handleEdit($index,row){
this.dialogVisible=true
this.news = row
},
// 定义提交事件判断是新增还是修改
onSubmit:function (news) {
if (news.id!=null){
this.newsUpdate();
this.news={};
}else {
this.newsAdds();
this.news={};
this.dialogVisible = false
}
},
//修改方法
newsUpdate(){
this.$request.post('/news/update', this.news)
.then((response)=> {
if (response===true){
this.$message({
message: '修改成功',
type: 'success'
})
this.getNewsPage()
}
})
//隐藏模态框
this.dialogVisible={};
this.dialogVisible = false
},
//删除操作
newsDelete(id){
this.$confirm('此操作将永久删除该文件, 是否继续?', '提示',{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$request.post("/news/delete?id="+id+"")
.then((res)=>{
if (res===true){
this.$message({
message: '删除成功',
type: 'success'
})
this.getNewsPage()
}else {
this.$message({
message: '删除失败',
type: 'success'
})
}
})
}).catch(() => {
this.$message({
type: 'info',
message: '已取消删除'
});
});
}
},
mounted() {
//默认时展示数据
//this.getNewsList();
//分页数据展示
this.getNewsPage()
//隐藏模态框
this.dialogVisible=false;
}
}
</script>
<style scoped>
</style>
后端
导入依赖
<dependencies>
<!--mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.0</version>
</dependency>
<!--druid 数据源 阿里巴巴提供的一款性能非常高的数据源-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- https:
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<!-- https:
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--jwt-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.8.0</version>
</dependency>
</dependencies>
application.properties
#配置数据源 druid springboot默认的数据源 HikariDataSource
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/newsdb?characterEncoding=utf8&useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
spring.datasource.username=root
spring.datasource.password=root
#指定数据源的类型
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
#日志的实现类
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
#已删除的值
mybatis-plus.global-config.db-config.logic-delete-value=1
#未删除的值
mybatis-plus.global-config.db-config.logic-not-delete-value=0
mybatis-plus.mapper-locations=classpath:com/ho/mapper/*.xml
server.port=8081
解决跨域问题
package com.ho.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedHeaders("*")
.allowedMethods("GET","POST","DELETE","HEAD","PUT","OPTIONS","DELETE")
.maxAge(3600)
.exposedHeaders("Server","Content-Length", "Authorization",
"Access-Token",
"Access-Control-Allow-Origin",
"Access-Control-Allow-Credentials",
"token",
"Access-Control-Allow-Origin");
}
}
配置分页插件
package com.ho.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class PageConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptorPage() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
配置 Spring Security
package com.ho.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/user/**").permitAll();
http.formLogin().loginPage("/user/login");
http.csrf().disable();
http.rememberMe();
}
}
dto下的R
package com.ho.dto;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
public class R {
@ApiModelProperty(value = "是否成功标示,true:成功;false:失败")
private String success;
@ApiModelProperty(value = "异常码")
private String errorCode;
@ApiModelProperty(value = "响应提示信息")
private String msg;
@ApiModelProperty(value = "返回数据")
private Object data;
private R(){}
public static R ok(){
R r = new R();
r.setSuccess("true");
r.setErrorCode(ResultCode.SUCCESS);
r.setMsg("成功");
return r;
}
public static R error(){
R r = new R();
r.setSuccess("false");
r.setErrorCode(ResultCode.ERROR);
r.setMsg("失败");
return r;
}
public R success(String success){
this.setSuccess(success);
return this;
}
public R message(String message){
this.setMsg(message);
return this;
}
public R code(String code){
this.setErrorCode(code);
return this;
}
public R data(Object data){
this.setData(data);
return this;
}
}
dto下的ResultCode
package com.ho.dto;
public interface ResultCode {
public static String SUCCESS = "00000";
public static String ERROR = "20001";
}
newsController
package com.ho.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ho.pojo.News;
import com.ho.service.NewsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
public class NewsController {
@Autowired
private NewsService newsService;
@RequestMapping("/news/list")
@ResponseBody
public Object list(@RequestParam(value = "pageNum",defaultValue = "1",required = false) Integer pageNum,
@RequestParam(value = "pageSize", defaultValue = "5", required = false) Integer pageSize) {
Page<News> page = new Page<>(pageNum, pageSize);
Page<News> userPage = newsService.page(page);
return userPage;
}
@RequestMapping("/news/add")
public Object NewsAdd(@RequestBody News news){
return newsService.save(news);
}
@RequestMapping("/news/delete")
public Object delete(@RequestParam("id") Integer id){
return newsService.removeById(id);
}
@RequestMapping("/news/update")
public boolean update(@RequestBody News news) {
QueryWrapper<News> queryWrapper = new QueryWrapper<News>();
queryWrapper.eq("id",news.getId());
return newsService.update(news,queryWrapper);
}
}
UserController
package com.ho.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ho.dto.R;
import com.ho.dto.ResultCode;
import com.ho.pojo.Users;
import com.ho.service.UsersService;
import com.ho.utils.JWTUtil;
import com.ho.vo.TokenVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
@RestController
public class UserController {
@Autowired
private UsersService usersService;
@RequestMapping("/user/login")
public String login(){
return "login";
}
private static final long EXPRIVE_TIME=30*60*1000;
@RequestMapping("/user/doLogin")
@ResponseBody
public Object doLogin(@RequestParam("username") String username, @RequestParam("password") String password,
HttpServletResponse response
) throws IOException {
QueryWrapper<Users> queryWrapper = new QueryWrapper<Users>();
queryWrapper.eq("u_user_name",username);
queryWrapper.eq("password",password);
Users users = usersService.getOne(queryWrapper);
if (users != null) {
String token = JWTUtil.sign(users.getUUserName(), String.valueOf(users.getId()));
TokenVo tokenVo=TokenVo.builder()
.token(token)
.genTime(System.currentTimeMillis())
.expTime(System.currentTimeMillis()+EXPRIVE_TIME).build();
R data = R.ok().data(tokenVo);
String tokenVoStr = new ObjectMapper().writeValueAsString(data);
response.setContentType("application/json;charset=utf-8");
response.setHeader("token", token);
PrintWriter out= new PrintWriter(new OutputStreamWriter(response.getOutputStream()));
out.write(tokenVoStr);
out.flush();
out.close();
}
return ResultCode.ERROR;
}
}
CheckTokenFilter
package com.ho.filer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ho.dto.R;
import com.ho.utils.JWTUtil;
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.*;
import java.util.Arrays;
import java.util.List;
@Component
public class CheckTokenFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
List<String> excludeUrl = Arrays.asList(
"/user/login",
"/user/doLogin"
);
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "POST");
response.setHeader("Access-Control-Allow-Headers", "*");
response.setHeader("Allow", "POST");
String requestURI = request.getRequestURI();
if(excludeUrl.contains(requestURI)){
filterChain.doFilter(request,response);
}
String token = request.getHeader("token");
System.err.println("==="+token);
if(!StringUtils.hasText(token)){
response.setContentType("application/json;charset=utf-8");
Writer out= new BufferedWriter(new OutputStreamWriter(response.getOutputStream()));
String tokenEmpty = new ObjectMapper().writeValueAsString(R.error().message("token为空"));
out.write(tokenEmpty);
out.flush();
out.close();
return;
}
if(!JWTUtil.verify(token)){
response.setContentType("application/json;charset=utf-8");
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.writeValue(response.getWriter(),
R.error().message("token不合法"));
return;
}
filterChain.doFilter(request,response);
}
}
Mapper
package com.ho.service;
import com.ho.pojo.Users;
import com.baomidou.mybatisplus.extension.service.IService;
public interface UsersService extends IService<Users>{
}
package com.ho.service;
import com.ho.pojo.News;
import com.baomidou.mybatisplus.extension.service.IService;
public interface NewsService extends IService<News>{
}
Service
package com.ho.service;
import com.ho.pojo.News;
import com.baomidou.mybatisplus.extension.service.IService;
public interface NewsService extends IService<News>{
}
package com.ho.service;
import com.ho.pojo.Users;
import com.baomidou.mybatisplus.extension.service.IService;
public interface UsersService extends IService<Users>{
}
package com.ho.service.impl;
import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ho.pojo.News;
import com.ho.mapper.NewsMapper;
import com.ho.service.NewsService;
@Service
public class NewsServiceImpl extends ServiceImpl<NewsMapper, News> implements NewsService{
}
package com.ho.service.impl;
import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ho.pojo.Users;
import com.ho.mapper.UsersMapper;
import com.ho.service.UsersService;
@Service
public class UsersServiceImpl extends ServiceImpl<UsersMapper, Users> implements UsersService{
}
JWTUtils
package com.ho.utils;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.interfaces.DecodedJWT;
import java.io.UnsupportedEncodingException;
import java.util.Date;
public class JWTUtil {
private static final long EXPRIE_TIME = 30*60*1000;
private static final String secret = "B8B7EDFB-53AC-2BD2-E804-461A0C961F29";
public static boolean verify(String token) {
try {
Algorithm algorithm = Algorithm.HMAC512(secret);
JWTVerifier verifier = JWT.require(algorithm)
.build();
verifier.verify(token);
return true;
} catch (Exception e) {
return false;
}
}
public static String getUsername(String token) {
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim("username").asString();
} catch (JWTDecodeException e) {
return null;
}
}
public static String getUserId(String token) {
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim("userId").asString();
} catch (JWTDecodeException e) {
return null;
}
}
public static String sign(String username,String userId) throws
UnsupportedEncodingException {
Date date = new Date(System.currentTimeMillis()+EXPRIE_TIME);
Algorithm algorithm = Algorithm.HMAC512(secret);
return JWT.create()
.withClaim("username", username)
.withClaim("userId", userId)
.withExpiresAt(date)
.sign(algorithm);
}
}
TokenVo
package com.ho.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data
@AllArgsConstructor
@NoArgsConstructor
@ApiModel(value = "Token响应对象")
@Builder
public class TokenVo implements Serializable {
private static final long serialVersionUID = 1959653550240034417L;
@ApiModelProperty(value = "用户认证凭证")
private String token;
@ApiModelProperty(value = "过期时间,单位毫秒")
private Long expTime;
@ApiModelProperty(value = "生成时间,单位毫秒")
private Long genTime;
}
pojo
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName(value = "news")
public class News {
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
@TableField(value = "type_id")
private String typeId;
@TableField(value = "title")
private String title;
@TableField(value = "publisher")
private String publisher;
@TableField(value = "publisher_face")
private String publisherFace;
@TableField(value = "content")
private String content;
@TableField(value = "create_time")
private Date createTime;
@TableField(value = "update_time")
private Date updateTime;
@TableField(value = "deleted")
private Integer deleted;
@TableField(value = "typeId")
private String typeid;
public static final String COL_ID = "id";
public static final String COL_TYPE_ID = "type_id";
public static final String COL_TITLE = "title";
public static final String COL_PUBLISHER = "publisher";
public static final String COL_PUBLISHER_FACE = "publisher_face";
public static final String COL_CONTENT = "content";
public static final String COL_CREATE_TIME = "create_time";
public static final String COL_UPDATE_TIME = "update_time";
public static final String COL_DELETED = "deleted";
public static final String COL_TYPEID = "typeId";
}
在SpringBoot3后台代码
Config
package com.yuan.spring_security.config;
import com.yuan.spring_security.filter.JwtAuthenticationFilter;
import com.yuan.spring_security.handler.CustomAccessDeniedHandler;
import com.yuan.spring_security.point.CustomAuthenticationEntryPoint;
import com.yuan.spring_security.service.MyUserDetailsService;
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.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableMethodSecurity
public class SecurityConfig {
@Autowired
private MyUserDetailsService userDetailsService;
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Autowired
private CustomAuthenticationEntryPoint customAuthenticationEntryPoint;
@Autowired
private CustomAccessDeniedHandler customAccessDeniedHandler;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/login", "/public").permitAll()
.anyRequest().authenticated()
)
.exceptionHandling(ex -> ex
.authenticationEntryPoint(customAuthenticationEntryPoint)
.accessDeniedHandler(customAccessDeniedHandler)
)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(HttpSecurity http, PasswordEncoder passwordEncoder) throws Exception {
AuthenticationManagerBuilder builder = http.getSharedObject(AuthenticationManagerBuilder.class);
builder.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
return builder.build();
}
}
AuthController
package com.yuan.spring_security.controller;
import com.yuan.spring_security.domain.Users;
import com.yuan.spring_security.utils.JwtUtils;
import com.yuan.spring_security.utils.TokenBlacklist;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping("/auth")
@CrossOrigin
public class AuthController {
@Autowired
private TokenBlacklist tokenBlacklist;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtUtils jwtUtils;
@PostMapping("/login")
public Map<String, String> login(@RequestBody Users users) {
Authentication auth = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(users.getUsername(), users.getPassword())
);
org.springframework.security.core.userdetails.User user = (org.springframework.security.core.userdetails.User) auth.getPrincipal();
String token = jwtUtils.generateToken(user.getUsername(), user.getAuthorities().iterator().next().getAuthority());
return Map.of("token", token);
}
@PostMapping("/logout")
public Map<String, String> logout(@RequestHeader("Authorization") String authHeader) {
if (authHeader != null && authHeader.startsWith("Bearer ")) {
String token = authHeader.substring(7);
tokenBlacklist.blacklistToken(token);
}
return Map.of("message", "Logged out successfully");
}
}
DemoController
package com.yuan.spring_security.controller;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
@RestController
@RequestMapping("/api")
@CrossOrigin
public class DemoController {
private List<String> testList = Arrays.asList("aaa", "bbb", "ccc");
@GetMapping("/public")
public String publicAccess() {
return "公开接口,谁都能访问";
}
@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/admin")
public String adminOnly() {
return "只有ADMIN角色能访问";
}
@PreAuthorize("hasRole('USER')")
@GetMapping("/user")
public String userOnly() {
return "只有USER角色能访问";
}
@PreAuthorize("hasAnyRole('ADMIN','USER')")
@GetMapping("/both")
public String bothRoles() {
return "ADMIN和USER都能访问";
}
@PreAuthorize("hasAnyRole('ADMIN','USER')")
@GetMapping("/test")
public String test() {
Collections.shuffle(testList);
System.out.println(testList.get(0));
return testList.get(0);
}
}
Users
package com.yuan.spring_security.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Users implements Serializable {
private Long id;
private String username;
private String password;
private String role;
}
JwtAuthenticationFilter
package com.yuan.spring_security.filter;
import com.yuan.spring_security.service.MyUserDetailsService;
import com.yuan.spring_security.utils.JwtUtils;
import com.yuan.spring_security.utils.TokenBlacklist;
import io.jsonwebtoken.Claims;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtUtils jwtUtils;
@Autowired
private MyUserDetailsService userDetailsService;
@Autowired
private TokenBlacklist tokenBlacklist;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
String header = request.getHeader("Authorization");
if (header != null && header.startsWith("Bearer ")) {
String token = header.substring(7);
if (tokenBlacklist.isBlacklisted(token)) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
try {
Claims claims = jwtUtils.parseToken(token);
String username = claims.getSubject();
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authToken);
if (jwtUtils.shouldRefresh(claims.getExpiration())) {
String role = (String) claims.get("role");
String newToken = jwtUtils.generateToken(username, role);
response.setHeader("X-Refresh-Token", newToken);
}
} catch (Exception e) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
}
chain.doFilter(request, response);
}
}
CustomAccessDeniedHandler
package com.yuan.spring_security.handler;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yuan.spring_security.service.ApiResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
ApiResponse<?> result = ApiResponse.fail(403, "权限不足,无法访问此资源");
response.getWriter().write(new ObjectMapper().writeValueAsString(result));
}
}
UsersMapper
package com.yuan.spring_security.mapper;
import com.yuan.spring_security.domain.Users;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@Mapper
public interface UsersMapper {
Users findByUsername(@Param("username") String username);
}
CustomAuthenticationEntryPoint
package com.yuan.spring_security.point;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yuan.spring_security.service.ApiResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
ApiResponse<?> result = ApiResponse.fail(401, "用户名密码错误、请重试");
response.getWriter().write(new ObjectMapper().writeValueAsString(result));
}
}
ApiResponse
package com.yuan.spring_security.service;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ApiResponse<T> {
private int code;
private String message;
private T data;
public static <T> ApiResponse<T> fail(int code, String message) {
return new ApiResponse<>(code, message, null);
}
}
MyUserDetailsService
package com.yuan.spring_security.service;
import com.yuan.spring_security.domain.Users;
import com.yuan.spring_security.mapper.UsersMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private UsersMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Users user = userMapper.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("用户不存在");
}
return org.springframework.security.core.userdetails.User.builder()
.username(user.getUsername())
.password(user.getPassword())
.roles(user.getRole().replace("ROLE_", ""))
.build();
}
}
JwtUtils
package com.yuan.spring_security.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets;
import java.util.Date;
@Component
public class JwtUtils {
private final String secretKey = "a8f7b36e5c9d4b12a1fef52d9c4a8e9d3f1b6a2c5d8e4f9a7b3c1d6e8f5a4c7d";
private final long expirationMs = 86400000;
private static final long EXPIRATION = 60 * 60 * 1000L;
private static final long REFRESH_THRESHOLD = 10 * 60 * 1000L;
public String generateToken(String username, String role) {
Date now = new Date();
Date expiry = new Date(now.getTime() + expirationMs);
return Jwts.builder()
.setSubject(username)
.claim("role", role)
.setIssuedAt(now)
.setExpiration(expiry)
.signWith(Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8)), SignatureAlgorithm.HS256)
.compact();
}
public Claims parseToken(String token) {
return Jwts.parserBuilder()
.setSigningKey(Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8)))
.build()
.parseClaimsJws(token)
.getBody();
}
public boolean shouldRefresh(Date expiration) {
long remaining = expiration.getTime() - System.currentTimeMillis();
return remaining < REFRESH_THRESHOLD;
}
}
TokenBlacklist
package com.yuan.spring_security.utils;
import org.springframework.stereotype.Component;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@Component
public class TokenBlacklist {
private final Set<String> blacklist = ConcurrentHashMap.newKeySet();
public void blacklistToken(String token) {
blacklist.add(token);
}
public boolean isBlacklisted(String token) {
return blacklist.contains(token);
}
}
server:
port: 8083
spring:
datasource:
url: jdbc:mysql://localhost:3306/security_demo?useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: root
logging:
level:
web: info
sql: debug
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.yuan.spring_security.domain
xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.yuan.spring_security.mapper.UsersMapper">
<select id="findByUsername" resultType="com.yuan.spring_security.domain.Users">
SELECT * FROM users WHERE username = #{username}
</select>
</mapper>