SpringSecurity前后端分离+JWT
1.什么是JWT
Json web token (JWT),是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC7519).该token被设计为紧凑且==安全==的,特别适用于==分布式站点的单点登录(SSO)场景==。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密
1.1 JWT结构
Header
Header 部分是一个JSON对象,描述JWT的元数据,通常是下面的样子。
{
"alg": "HS256",
"typ": "JWT"
}
上面代码中,alg属性表示签名的算法(algorithm),默认是 HMAC SHA256 (写成 HS256) ;typ属性表示这个令牌(token)的类型(type), JWT令牌统一写为JWT。
最后,将上面的JSON对象使用Base64URL算法转成字符串。
Payload(荷载)
Payload 部分也是一个JSON对象,==用来存放实际需要传递的数据==。JWT规定了7个官方字段,供选用。
iss (issuer):签发人
exp (expiration time):过期时间
sub (subject):主题
aud (audience):受众
nbf (Not Before):生效时间
iat (lssued At):签发时间
jti (JWT ID):编号
除了官方字段,==你还可以在这个部分定义私有字段==,下面就是一个例子。
{
"sub": "1234567890",
"name" : "John Doe",
“userid”:2
"admin": true
}
注意,JWT 默认是不加密的,任何人都可以读到,所以不要把==秘密信息==放在这个部分。这个JSON 对象也要使用Base64URL 算法转成字符串。
Signature
Signature部分是对前两部分的签名,防止数据篡改。
首先,需要指定一个==密钥(secret)==。这个密钥只有==服务器才知道==,不能泄露给用户。然后,使用Header里面指定的==签名算法(默认是 HMAC SHA256)==,按照下面的公式产生签名。
HMACSHA256(
base64UrlEncode(header) + ".”"+base64UrlEncode(payload),
secret)
算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以返回给用户。
【注 :在其中荷载部分最主要,因为要将用户的信息存入其中】
2. 前端请求携带token
登录成功时将token存储到localStorage中
this.$message.success(re.data.msg) localStorage.setItem("token",re.data.data) //将token存储到localStorage中 this.$router.push("/test")
main.js配置文件中配置请求拦截器,将token放在请求头中:
【注:登录后的每次请求都会携带着token】
import axios from 'axios' Vue.prototype.axios = axios //请求拦截器 axios.interceptors.request.use(config => { if(localStorage.getItem("token")){ config.headers['token']=localStorage.getItem("token") } return config; },function(error){ return Promise.reject(error); });
3. 后端配置security
创建新的spring工程
pom文件:
<dependencies> <!-- //hutu工具类依赖--> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.16</version> </dependency> <!-- JWT令牌依赖--> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> <!--pageHelper的依赖--> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.4.5</version> </dependency> <!--引入swagger2依赖--> <dependency> <groupId>com.spring4all</groupId> <artifactId>swagger-spring-boot-starter</artifactId> <version>1.9.1.RELEASE</version> </dependency> <!--图形化依赖--> <dependency> <groupId>com.github.xiaoymin</groupId> <artifactId>swagger-bootstrap-ui</artifactId> <version>1.9.6</version> </dependency> <!--mp的依赖--> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.1</version> </dependency> <!-- 数据库连接池--> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.2.1</version> </dependency> <!-- security依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- mysql--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <!-- lombok--> <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> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency> </dependencies>
3.1 Config配置层
3.1.1 前后端跨域
GlobalCorsConfig配置类
@Configuration public class GlobalCorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins("*") .allowCredentials(true) .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .maxAge(3600); } }
3.1.2 security配置
SecurityConfig配置类
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { //创建密码加密器 @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); }; //jwtfile过滤器 @Autowired private JwtTokenFilter jwtTokenFilter; @Resource private LogEmpServiceim logEmpServiceim; //构建认证的账户和权限 @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(logEmpServiceim).passwordEncoder(passwordEncoder()); } @Override protected void configure(HttpSecurity http) throws Exception { //放行资源登录表单资源 http.formLogin() //表单提交action的方法 .loginProcessingUrl("/log") //表单对应的账号 .usernameParameter("userName") //表单对应的密码 .passwordParameter("password") //登录成功后跳转到的路径,默认跳转之前的路径 .successHandler(successHandler()) //登录失败后跳转的页面 .failureHandler(failureHandler()) .and() //放行默认的跳转的路径 .authorizeRequests().antMatchers("/log","/error.html").permitAll() //其他的路径 都是需要经过认证的 .anyRequest().authenticated() .and() //权限不足跳转的路径 .exceptionHandling() .accessDeniedHandler(accessDeniedHandler()) .and() // 防护默认开启。默认会拦截请求如果token和服务端的token匹配成功,则正常访问 .csrf() //禁用;关闭;禁止 .disable(); //允许跨域 http.cors(); //植入jwtfile过滤器 http.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class); } @Override public void configure(WebSecurity web) throws Exception { super.configure(web); } //登录成功后返回的结果 @Bean public AuthenticationSuccessHandler successHandler() { return new AuthenticationSuccessHandler() { @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { //获取用户信息 User user=(User) authentication.getPrincipal(); //获取用户的资源信息 List<String> collect = user.getAuthorities().stream().map(grantedAuthority -> grantedAuthority.getAuthority()).collect(Collectors.toList()); //通过jwt生成token令牌且将用户信息和用户资源封装进去 HashMap<String, Object> stringObjectHashMap = new HashMap<>(); stringObjectHashMap.put("username",user.getUsername()); stringObjectHashMap.put("CollectMessage",collect); String token = JwtUtils.generateJwt(stringObjectHashMap); result result = new result(200, "认证成功", token); response.setContentType("application/json;charset=utf8"); ObjectMapper objectMapper = new ObjectMapper(); String value = objectMapper.writeValueAsString(result); //响应前端 PrintWriter writer = response.getWriter(); writer.print(value); //刷新缓存区 writer.flush(); //关闭缓存区 writer.close(); } }; } //登录失败返回的结果 @Bean public AuthenticationFailureHandler failureHandler() { return new AuthenticationFailureHandler() { @Override public void onAuthenticationFailure(HttpServletRequest Request, HttpServletResponse Response, AuthenticationException e) throws IOException, ServletException { result result = new result(500, "账号或密码错误", e); Response.setContentType("application/json;charset=utf8"); //将result封装到object ObjectMapper objectMapper = new ObjectMapper(); String value = objectMapper.writeValueAsString(result); //响应前端 PrintWriter writer = Response.getWriter(); writer.print(value); //刷新缓存区 writer.flush(); //关闭进程 writer.close(); } }; } //权限不足返回的结果 @Bean public AccessDeniedHandler accessDeniedHandler() { return new AccessDeniedHandler() { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException { result result = new result(403, "权限不足", e); response.setContentType("application/json;charset=utf8"); ObjectMapper objectMapper = new ObjectMapper(); String s = objectMapper.writeValueAsString(result); //响应前端 PrintWriter writer = response.getWriter(); writer.print(s); //刷新缓存区 writer.flush(); //关闭进程 writer.close(); } }; } }
3.1.3 swagger配置
DocConfig配置类
@Configuration public class DocConfig { @Bean public Docket docket(){ Docket docket=new Docket(DocumentationType.SWAGGER_2) .apiInfo(getInfo()) .select() .apis(RequestHandlerSelectors.basePackage("com.aaa.controller")) .build(); return docket; } private ApiInfo getInfo(){ Contact DEFAULT_CONTACT = new Contact("百晓生", "http://www.wjy.com", "110@qq.com"); ApiInfo info = new ApiInfo("AAA", "vue管理系统", "v1.0", "http://www.jd.com", DEFAULT_CONTACT, "Apache 2.0", "http://www.baidu.com", new ArrayList<VendorExtension>()); return info; } }
3.1.4 MybatisPlass配置
MPConfig配置类
@Configuration public class MpConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }
3.2 Util工具层
JWTUtil工具类
public class JwtUtils { private static String signKey = "ixwixw"; private static long expire = 43200000L; /** * 生成jwt令牌 */ public static String generateJwt(Map<String,Object> claims){ String jwt = Jwts.builder() .addClaims(claims) .signWith(SignatureAlgorithm.HS256,signKey) .setExpiration(new Date(System.currentTimeMillis() + expire)) .compact(); return jwt; } /** * 解析jwt令牌 */ public static Claims parseJWT(String jwt){ Claims claims = Jwts.parser() .setSigningKey(signKey) .parseClaimsJws(jwt) .getBody(); return claims; } }
3.4 Filter层
JwtTokenFilter过滤器类
//定义Jwt过滤器 @Component public class JwtTokenFilter extends OncePerRequestFilter { //不登录就可以访问的路径 String[] withname = {"/log"}; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String token = request.getHeader("Token"); //获取请求的路径 String requestURI = request.getRequestURI(); //判断token是否为空 if (StringUtils.isEmpty(token)){ //如果为空 // requestURI 是否包含在whitename 里面 if(ArrayUtil.contains(withname,requestURI)){ //如果在白名单内直接放行 filterChain.doFilter(request,response); }else { //如果不在直接提示 result result = new result(500,"没有登录",null); printJsonData(result,response); } return; } //不为空则校验token Claims claims = JwtUtils.parseJWT(token); Object username; try { // 用户名 username = claims.get("username"); List<String> res = (List<String>) claims.get("CollectMessage"); // List<SimpleGrantedAuthority> collect = res.stream().map(s -> new SimpleGrantedAuthority(s)).collect(Collectors.toList()); // 密码 // 资源的信息 UsernamePasswordAuthenticationToken usertoken = new UsernamePasswordAuthenticationToken(username, null, collect); // 存起来用户的信息 SecurityContextHolder.getContext().setAuthentication(usertoken); //放行 filterChain.doFilter(request, response); }catch (Exception e){ e.printStackTrace(); throw new RuntimeException("token非法"); } } public void printJsonData(result result,HttpServletResponse response) throws IOException { // 以json的形式 传递除去 ObjectMapper objectMapper = new ObjectMapper(); String s = objectMapper.writeValueAsString(result); // 响应到前端 response.setContentType("application/json;charset=utf8"); PrintWriter writer = response.getWriter(); writer.write(s); writer.flush(); writer.close(); } }
3.5 entity实体层
3.5.1 dept实体
@Data @NoArgsConstructor @AllArgsConstructor public class dept { @TableId(value = "eid") private int eid; private String depa; }
3.5.2 emp实体
@Data @AllArgsConstructor @NoArgsConstructor @ToString @ApiModel(value = "员工实体类") public class emp { @ApiModelProperty(value = "员工id") private int id; @ApiModelProperty(value = "员工姓名") private String name; @ApiModelProperty(value = "员工入职时间") private Date date; @ApiModelProperty(value = "员工地址") private String address; @ApiModelProperty(value = "员工账号") @TableField(value = "user_name") private String userName; @ApiModelProperty(value = "员工密码") private String password; @ApiModelProperty(value = "链表id") private int deptid; @TableField(exist = false) private dept dept; }
3.5.3 menu实体
@Data @AllArgsConstructor @NoArgsConstructor @ToString public class menu { @TableId private int mid; @TableField("menu_name") private String menuName; @TableField("pem_key") private String pemKey; }
3.6 dao层
3.6.1 DeptDao 部门
public interface DeptDao extends BaseMapper<dept> { }
3.6.2 EmpDao 员工
public interface EmpDao extends BaseMapper<emp> { //员工&部门链表查询 IPage<emp> seleall(IPage<emp> page, @Param("ew") Wrapper<emp> queryWrapper);//这里的泛型根据自己的实体类变化 }
3.6.3 MenuDao 菜单权限
public interface MenuDao extends BaseMapper<menu> { public List<menu> SelectMenu(Integer eid); }
3.7 Mapper映射层
3.7.1 EmpMapper 员工
<?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"> <!--namespace必须和dao接口的名称一模一样--> <mapper namespace="com.aaa.dao.EmpDao"> <resultMap id="EmpMap" type="com.aaa.entity.emp" autoMapping="true"> <id column="id" property="id"/> <association property="dept" javaType="com.aaa.entity.dept" autoMapping="true"> <id column="eid" property="eid"/> </association> </resultMap> <select id="seleall" resultMap="EmpMap"> SELECT * from emp JOIN dept on emp.deptid = dept.eid </select> </mapper>
3.7.2 MenuMapper 菜单
<?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"> <!--namespace必须和dao接口的名称一模一样--> <mapper namespace="com.aaa.dao.MenuDao"> <select id="SelectMenu" resultType="com.aaa.entity.menu"> SELECT menu.* FROM `menu` JOIN role_menu on role_menu.mid = menu.mid join emp_role on emp_role.rid = role_menu.rid WHERE emp_role.eid = #{eid} </select> </mapper>
3.8 Service层
3.8.1 EmpService接口层
public interface EmpService {
result seleall(Integer page,Integer size);
//登录
result log(emp emp);
//退出登录
result exitlog();
//删除员工信息
result del(Integer id);
//修改员工信息
result update(emp emp);
//查询所有部门
result seledeptall();
}
3.8.2 EmpServiceim接口实现
@Service
public class EmpServiceim implements EmpService {
@Autowired
private EmpDao empDao;
@Autowired
private DeptDao dao;
@Override
public result seleall(Integer page,Integer size) {
IPage<emp> ipage = new Page<>(page,size);
IPage<emp> empIPage = empDao.seleall(ipage,null);
return empIPage!=null?new result(200,"查询成功",empIPage):new result(500,"查询失败",null);
}
//登录
@Override
public result log(emp emp) {
QueryWrapper<emp> wrapper = new QueryWrapper<>();
wrapper.eq("user_name",emp.getUserName());
wrapper.eq("password",emp.getPassword());
List<com.aaa.entity.emp> emps = empDao.selectList(wrapper);
return !emps.isEmpty()?new result(200,"登录成功",null):new result(500,"登录失败",null);
}
//退出登录
@Override
public result exitlog() {
//获取SecurityContextHolder中的用户id
// UsernamePasswordAuthenticationToken authenticationToken = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext();
// LogEmp logEmp = (LogEmp) authenticationToken.getPrincipal();
// int empid = logEmp.getEmp().getId();
// //删除Redis中的值
// redisTemplate.delete("login"+empid);
return new result(200,"注销成功",null);
}
//根据id删除员工信息
@Override
public result del(Integer id) {
int i = empDao.deleteById(id);
return i==1?new result(200,"删除成功",null):new result(500,"删除失败",null);
}
//修改员工信息
@Override
public result update(emp emp) {
int update = empDao.updateById(emp);
return update==1?new result(200,"修改成功",null):new result(500,"修改失败",null);
}
//查询所有部门
@Override
public result seledeptall() {
List<dept> depts = dao.selectList(null);
return depts!=null?new result(200,"查询成功",depts):new result(500,"查询失败",null);
}
}
3.8.3 LogEmpServiceim接口实现
【注:此实现类专门用户处理前端传来的登录信息进行判断】
【注:此实现类实现的是
UsernameNotFoundException接口
】@Service public class LogEmpServiceim implements UserDetailsService { @Autowired private EmpDao empDao; @Autowired private MenuDao menuDao; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //1、根据账户查询用户信息 QueryWrapper<emp> wrapper = new QueryWrapper<>(); wrapper.eq("user_name",username); emp user = empDao.selectOne(wrapper); if(user==null){ System.out.println("账号错误"); }else{ //2、查询用户具有的权限 List<menu> permissions = menuDao.SelectMenu(user.getId()); //返回UserDetails对象--它是一个接口,返回他的实现类 /** * String username,账户 * String password,密码 * Collection<? extends GrantedAuthority> authorities 该用户具有的权限 */ Collection<? extends GrantedAuthority> authorities=permissions.stream() .map(item-> new SimpleGrantedAuthority(item.getPemKey())).collect(Collectors.toList()); User u = new User(username,user.getPassword(),authorities); return u; } return null; } }
3.9 controller层
@RestController @Api(tags = "员工管理") public class EmpController { @Autowired private EmpService empService; /** * 查询员工全部信息 * @param page * @param size * @return */ @ApiOperation(value = "查询员工全部信息") @ApiImplicitParams(value = {@ApiImplicitParam(name = "page", value = "分页当前页数", required = true, dataType = "int"), @ApiImplicitParam(name = "size", value = "分页当前条数", required = true, dataType = "int")}) @PreAuthorize(value = "hasAuthority('emp:sele')") @GetMapping("/sele") public result sele(Integer page,Integer size){ return empService.seleall(page,size); } /** * 退出登录 */ @GetMapping("/logout") public result logout(){ return empService.exitlog(); } /** * 查询所有部门信息 * @return */ @GetMapping("/deptall") public result seledept(){ return empService.seledeptall(); } /** * 删除员工信息 * @param id * @return */ @GetMapping("/del") @PreAuthorize(value = "hasAuthority('emp:del')") public result del(Integer id){ return empService.del(id); } @PreAuthorize(value = "hasAuthority('emp:update')") @PostMapping("/update") public result update(@RequestBody emp emp){ return empService.update(emp); } }
【注:注解@PreAuthorize(value = "hasAuthority('emp:sele')")】【其用来限制权限信息:emp:sele】
【注:必须在主启动上开启Security注解其才会生效
@EnableGlobalMethodSecurity(prePostEnabled = true)//开启Security注解
】
【注:此注解的权限信息是根据数据库表中的权限/菜单表查询出的,一定确保此注解上的权限Key与数据库中的权限/菜单表的Key一致】
3.10 主启动类
@SpringBootApplication @MapperScan(basePackages = "com.aaa.dao") @EnableSwagger2 @EnableGlobalMethodSecurity(prePostEnabled = true)//开启Security注解 public class BeforSecurityProjectApplication { public static void main(String[] args) { SpringApplication.run(BeforSecurityProjectApplication.class, args); } }
3.11 application配置类
数据连接信息与MybatisPlass打印日志等配置
spring.datasource.druid.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.druid.username=root spring.datasource.druid.password=root spring.datasource.druid.url=jdbc:mysql://localhost:3306/vuesql?serverTimezone=Asia/Shanghai mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
4. 数据库信息
【注:源码和数据库信息都已经放在了Gitee仓库中:链接信息: BeforeSecurityProject 】
以上便是SpringSecurity前后端分离+JWT中的内容,如有漏缺请在下方留言告知,我会及时补充