关注公号:java大师,回复“图书”,获取源码
1.1添加pom.xml
org.projectlombok
lombok
com.baomidou
mybatis-plus-boot-starter
3.4.3.1
com.baomidou
mybatis-plus-generator
3.1.0
org.freemarker
freemarker
2.3.31
mysql
mysql-connector-java
8.0.28
org.apache.commons
commons-lang3
3.7
1.2创建CodeGenerator代码生成类
package com.ds.book.mp;
import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.InjectionConfig;
import com.baomidou.mybatisplus.generator.config.*;
import com.baomidou.mybatisplus.generator.config.po.TableInfo;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;
import org.apache.commons.lang3.StringUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
public class CodeGenerator {
/**
*
* 读取控制台内容
*
*/
public static String scanner(String tip) {
Scanner scanner = new Scanner(System.in);
StringBuilder help = new StringBuilder();
help.append("请输入" + tip + ":");
System.out.println(help.toString());
if (scanner.hasNext()) {
String ipt = scanner.next();
if (StringUtils.isNotBlank(ipt)) {
return ipt;
}
}
throw new MybatisPlusException("请输入正确的" + tip + "!");
}
public static void main(String[] args) {
// 代码生成器
AutoGenerator mpg = new AutoGenerator();
// 全局配置
GlobalConfig gc = new GlobalConfig();
String projectPath = System.getProperty("user.dir");
gc.setOutputDir(projectPath + "/src/main/java");
gc.setAuthor("java大师");
gc.setOpen(false);
// gc.setSwagger2(true); 实体属性 Swagger2 注解
mpg.setGlobalConfig(gc);
// 数据源配置
DataSourceConfig dsc = new DataSourceConfig();
dsc.setUrl("jdbc:mysql://175.24.198.63:3306/book?useSSL=false&characterEncoding=utf8&serverTimezone=GMT%2B8");
// dsc.setSchemaName("public");
dsc.setDriverName("com.mysql.cj.jdbc.Driver");
dsc.setUsername("root");
dsc.setPassword("root@1234!@#");
mpg.setDataSource(dsc);
// 包配置
PackageConfig pc = new PackageConfig();
// pc.setModuleName(scanner("模块名"));
pc.setParent("com.ds.book");
mpg.setPackageInfo(pc);
// 自定义配置
InjectionConfig cfg = new InjectionConfig() {
@Override
public void initMap() {
// to do nothing
}
};
// 如果模板引擎是 freemarker
String templatePath = "/templates/mapper.xml.ftl";
// 如果模板引擎是 velocity
// String templatePath = "/templates/mapper.xml.vm";
// 自定义输出配置
List focList = new ArrayList<>();
// 自定义配置会被优先输出
focList.add(new FileOutConfig(templatePath) {
@Override
public String outputFile(TableInfo tableInfo) {
// 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!!
return projectPath + "/src/main/resources/mapper/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML;
}
});
/*
cfg.setFileCreate(new IFileCreate() {
@Override
public boolean isCreate(ConfigBuilder configBuilder, FileType fileType, String filePath) {
// 判断自定义文件夹是否需要创建
checkDir("调用默认方法创建的目录,自定义目录用");
if (fileType == FileType.MAPPER) {
// 已经生成 mapper 文件判断存在,不想重新生成返回 false
return !new File(filePath).exists();
}
// 允许生成模板文件
return true;
}
});
*/
cfg.setFileOutConfigList(focList);
mpg.setCfg(cfg);
// 配置模板
TemplateConfig templateConfig = new TemplateConfig();
// 配置自定义输出模板
//指定自定义模板路径,注意不要带上.ftl/.vm, 会根据使用的模板引擎自动识别
// templateConfig.setEntity("templates/entity2.java");
// templateConfig.setService();
// templateConfig.setController();
templateConfig.setXml(null);
mpg.setTemplate(templateConfig);
// 策略配置
StrategyConfig strategy = new StrategyConfig();
strategy.setNaming(NamingStrategy.underline_to_camel);
strategy.setColumnNaming(NamingStrategy.underline_to_camel);
strategy.setTablePrefix("t_");
// strategy.setInclude("t_user");
// strategy.setSuperEntityClass("你自己的父类实体,没有就不用设置!");
strategy.setEntityLombokModel(true);
strategy.setRestControllerStyle(true);
// 公共父类
// strategy.setSuperControllerClass("你自己的父类控制器,没有就不用设置!");
// 写于父类中的公共字段
strategy.setSuperEntityColumns("id");
strategy.setInclude(scanner("表名,多个英文逗号分割").split(","));
strategy.setControllerMappingHyphenStyle(true);
// strategy.setTablePrefix(pc.getModuleName() + "_");
mpg.setStrategy(strategy);
mpg.setTemplateEngine(new FreemarkerTemplateEngine());
mpg.execute();
}
}
1.3生成crontroller、service、mapper、entity等业务实体类
运行CodeGenerator,生成业务实体类
请输入表名,多个英文逗号分割: t_user,t_menu,t_role,t_user_role,t_role_menu
2.1整合springsecurity
1)
org.springframework.boot
spring-boot-starter-security
2.2认证授权流程
认证管理
流程图解读:
1、用户提交用户名、密码被SecurityFilterChain中的 UsernamePasswordAuthenticationFilter 过滤器获取到, 封装为请求Authentication,通常情况下是UsernamePasswordAuthenticationToken这个实现类。
2、然后过滤器将Authentication提交至认证管理器(AuthenticationManager)进行认证 。
3、认证成功后, AuthenticationManager 身份管理器返回一个被填充满了信息的(包括上面提到的权限信息, 身份信息,细节信息,但密码通常会被移除) Authentication 实例。
4、SecurityContextHolder 安全上下文容器将第3步填充了信息的 Authentication ,通过 SecurityContextHolder.getContext().setAuthentication(…)方法,设置到其中。 可以看出AuthenticationManager接口(认证管理器)是认证相关的核心接口,也是发起认证的出发点,它 的实现类为ProviderManager。而Spring Security支持多种认证方式,因此ProviderManager维护着一个 List 列表,存放多种认证方式,最终实际的认证工作是由 AuthenticationProvider完成的。咱们知道web表单的对应的AuthenticationProvider实现类为 DaoAuthenticationProvider,它的内部又维护着一个UserDetailsService负责UserDetails的获取。最终 AuthenticationProvider将UserDetails填充至Authentication。
授权管理
访问资源(即授权管理),访问url时,会通过FilterSecurityInterceptor拦截器拦截,其中会调用SecurityMetadataSource的方法来获取被拦截url所需的全部权限,再调用授权管理器AccessDecisionManager,这个授权管理器会通过spring的全局缓存SecurityContextHolder获取用户的权限信息,还会获取被拦截的url和被拦截url所需的全部权限,然后根据所配的投票策略(有:一票决定,一票否定,少数服从多数等),如果权限足够,则决策通过,返回访问资源,请求放行,否则跳转到403页面、自定义页面。
2.3编写自己的UserDetails和UserDetailService
2.3.1UserDetails
package com.ds.book.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
import java.util.Collection;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
/**
*
*
*
*
* @author java大师
* @since 2023-03-17
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("t_user")
public class User implements Serializable, UserDetails {
private static final long serialVersionUID = 1L;
private Integer id;
/**
* 登录名
*/
private String name;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 是否有效:1-有效;0-无效
*/
private String status;
@Override
public Collection extends GrantedAuthority> getAuthorities() {
return roles
.stream()
.map(role -> new SimpleGrantedAuthority(role.getRoleCode()))
.collect(Collectors.toList());
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
2.3.2userDetailService
登录成功后,将UserDetails的roles设置到用户中
package com.ds.book.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.ds.book.entity.User;
import com.ds.book.mapper.UserMapper;
import com.ds.book.service.IUserService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.lang3.StringUtils;
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;
/**
*
* 服务实现类
*
*
* @author java大师
* @since 2023-03-17
*/
@Service
public class UserServiceImpl extends ServiceImpl implements IUserService, UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User loginUser = userMapper.selectOne(new QueryWrapper().eq("username", username));
if (loginUser == null){
throw new UsernameNotFoundException("用户名或密码错误");
}
loginUser.setRoles(userMapper.getRolesByUserId(loginUser.getId()));
return loginUser;
}
}
2.3.2加载userDetailService
将我们自己的UserDetailService注入springsecurity
package com.ds.book.config;
import com.ds.book.filter.JwtTokenFilter;
import com.ds.book.service.impl.UserServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserServiceImpl userService;
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
//注入我们自己的UserDetailService
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
}
}
问题:前后端分离项目,通常不会使用springsecurity自带的登录界面,登录界面由前端完成,后台只需要提供响应的服务即可,且目前主流不会采用session去存取用户,后端会返回响应的token,前端访问的时候,会在headers里面带入token.
2.4JwtToken
2.4.1 JWT描述
Jwt token由Header、Payload、Signature三部分组成,这三部分之间以小数点”.”连接,JWT token长这样:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.keH6T3x1z7mmhKL1T3r9sQdAxxdzB6siemGMr_6ZOwU
token解析后长这样: header部分,有令牌的类型(JWT)和签名算法名称(HS256): { "alg": "HS256", "typ": "JWT" } Payload部分,有效负载,这部分可以放任何你想放的数据:
{ "sub": "1234567890", "name": "John Doe", "iat": 1516239022 }
Signature签名部分,由于这部分是使用header和payload部分计算的,所以还可以以此来验证payload部分有没有被篡改:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
123456 //这里是密钥,只要够复杂,一般不会被破解
)
2.4.2 pom.xml
io.jsonwebtoken
jjwt
0.9.0
2.4.3 JwtToken工具类
package com.ds.book.tool;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
import java.util.Date;
import java.util.UUID;
/**
* JWT工具类
*/
public class JwtUtil {
//有效期为
public static final Long JWT_TTL = 60 * 60 *1000L;// 60 * 60 *1000 一个小时
//设置秘钥明文
public static final String JWT_KEY = "dashii";
public static String getUUID(){
String token = UUID.randomUUID().toString().replaceAll("-", "");
return token;
}
/**
* 生成jtw
* @param subject token中要存放的数据(json格式)
* @return
*/
public static String createJWT(String subject) {
JwtBuilder builder = getJwtBuilder(subject, null, getUUID());// 设置过期时间
return builder.compact();
}
/**
* 生成jtw
* @param subject token中要存放的数据(json格式)
* @param ttlMillis token超时时间
* @return
*/
public static String createJWT(String subject, Long ttlMillis) {
JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());// 设置过期时间
return builder.compact();
}
private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
SecretKey secretKey = generalKey();
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
if(ttlMillis==null){
ttlMillis= JwtUtil.JWT_TTL;
}
long expMillis = nowMillis + ttlMillis;
Date expDate = new Date(expMillis);
return Jwts.builder()
.setId(uuid) //唯一的ID
.setSubject(subject) // 主题 可以是JSON数据
.setIssuer("dashi") // 签发者
.setIssuedAt(now) // 签发时间
.signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥
.setExpiration(expDate);
}
/**
* 创建token
* @param id
* @param subject
* @param ttlMillis
* @return
*/
public static String createJWT(String id, String subject, Long ttlMillis) {
JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);// 设置过期时间
return builder.compact();
}
public static void main(String[] args) throws Exception {
String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJjYWM2ZDVhZi1mNjVlLTQ0MDAtYjcxMi0zYWEwOGIyOTIwYjQiLCJzdWIiOiJzZyIsImlzcyI6InNnIiwiaWF0IjoxNjM4MTA2NzEyLCJleHAiOjE2MzgxMTAzMTJ9.JVsSbkP94wuczb4QryQbAke3ysBDIL5ou8fWsbt_ebg";
Claims claims = parseJWT(token);
System.out.println(claims);
}
/**
* 生成加密后的秘钥 secretKey
* @return
*/
public static SecretKey generalKey() {
byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
return key;
}
/**
* 解析
*
* @param jwt
* @return
* @throws Exception
*/
public static Claims parseJWT(String jwt) throws Exception {
SecretKey secretKey = generalKey();
return Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(jwt)
.getBody();
}
}
2.4.4 JwtTokenFilter
package com.ds.book.filter;
import com.ds.book.entity.User;
import com.ds.book.mapper.UserMapper;
import com.ds.book.service.IMenuService;
import com.ds.book.service.IUserService;
import com.ds.book.tool.JwtUtil;
import io.jsonwebtoken.Claims;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
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;
@Component
public class JwtTokenFilter extends OncePerRequestFilter {
@Autowired
private IUserService userService;
@Autowired
private UserMapper userMapper;
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
//1、获取token
String token = httpServletRequest.getHeader("token");
if (StringUtils.isEmpty(token)){
filterChain.doFilter(httpServletRequest,httpServletResponse);
return;
}
String userId;
try {
Claims claims = JwtUtil.parseJWT(token);
userId = claims.getSubject();
} catch (Exception exception) {
exception.printStackTrace();
throw new RuntimeException("token非法");
}
User user = userService.getUserById(Integer.parseInt(userId));
user.setRoles(userMapper.getRolesByUserId(Integer.parseInt(userId)));
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(user,null,user.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
filterChain.doFilter(httpServletRequest,httpServletResponse);
}
}
在springsecurity中,第一个经过的过滤器是UsernamePasswordAuthenticationFilter,所以前后端分离的项目,我们自己定义的过滤器要放在这个过滤器前面,具体配置如下
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/login").permitAll()
.anyRequest().authenticated();
http.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class);
http.cors();
}
2.4.5授权
2.4.5.1 开启preAuthorize进行收取(Controller路径匹配)
1)主启动类上添加EnableGlobalMethodSecurity注解
@EnableGlobalMethodSecurity(prePostEnabled = true)
@SpringBootApplication
@MapperScan("com.ds.book.mapper")
public class BookSysApplication {
public static void main(String[] args) {
SpringApplication.run(BookSysApplication.class,args);
}
}
2)Controller方法上添加@PreAuthorize注解
@RestController
public class HelloController {
@GetMapping("/hello")
@PreAuthorize("hasRole('ROLE_ADMIN')")
public String hello(){
return "hello";
}
}
2.4.5.2 增强方式授权(数据库表配置)
1)创建我们自己的FilterInvocationSecurityMetadataSource,实现getAttributes方法,获取请求url所需要的角色
@Component
public class MySecurtiMetaDataSource implements FilterInvocationSecurityMetadataSource {
@Autowired
private IMenuService menuService;
AntPathMatcher antPathMatcher = new AntPathMatcher();
//获取访问url需要的角色,例如:/sys/user需要ROLE_ADMIN角色,访问sys/user时获取到必须要有ROLE_ADMIN角色。返回 Collection
@Override
public Collection getAttributes(Object object) throws IllegalArgumentException {
String requestURI = ((FilterInvocation) object).getRequest().getRequestURI();
//获取所有的菜单及角色
List
2)创建我们自己的决策管理器AccessDecisionManager,实现decide方法,判断步骤1)中获取到的角色和我们目前登录的角色是否相同,相同则允许访问,不相同则不允许访问,
@Component
public class MyAccessDecisionManager implements AccessDecisionManager {
//1、认证通过后,会往authentication中填充用户信息
//2、拿authentication中的权限与上一步获取到的角色信息进行比对,比对成功后,允许访问
@Override
public void decide(Authentication authentication, Object object, Collection configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
Collection extends GrantedAuthority> authorities = authentication.getAuthorities();
for (ConfigAttribute configAttribute : configAttributes) {
for (GrantedAuthority authority : authorities) {
if (authority.getAuthority().equals(configAttribute.getAttribute())){
return;
}
}
}
throw new AccessDeniedException("权限不足,请联系管理员");
}
@Override
public boolean supports(ConfigAttribute attribute) {
return false;
}
@Override
public boolean supports(Class> clazz) {
return false;
}
}
3)在SecurityConfig中,添加后置处理器(增强器),让springsecurity使用我们自己的datametasource和decisionMananger
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MySecurtiMetaDataSource mySecurtiMetaDataSource;
@Autowired
private MyAccessDecisionManager myAccessDecisionManager;
@Autowired
private MyAuthenticationEntryPoint myAuthenticationEntryPoint;
@Autowired
private MyAccessDeniedHandler myAccessDeniedHandler;
@Autowired
private UserServiceImpl userService;
@Autowired
private JwtTokenFilter jwtTokenFilter;
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/login").permitAll()
.anyRequest().authenticated()
//后置处理器,使用我们自己的FilterSecurityInterceptor拦截器配置
.withObjectPostProcessor(new ObjectPostProcessor () {
@Override
public O postProcess(O o) {
o.setSecurityMetadataSource(mySecurtiMetaDataSource);
o.setAccessDecisionManager(myAccessDecisionManager);
return o;
}
})
.and()
.headers().cacheControl();
http.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class);
http.cors();
}
}
2.4.6异常处理
1)前端渲染工具类
public class WebUtils
{
/**
* 将字符串渲染到客户端
*
* @param response 渲染对象
* @param string 待渲染的字符串
* @return null
*/
public static String renderString(HttpServletResponse response, String string) {
try
{
response.setStatus(200);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().print(string);
}
catch (IOException e)
{
e.printStackTrace();
}
return null;
}
}
2)未登录异常处理,实现commence方法
@Component
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
Result result = new Result(401,"未登录,请先登录",null);
String json = JSON.toJSONString(result);
WebUtils.renderString(httpServletResponse,json);
}
}
3)授权失败异常处理,实现Handle方法
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
Result result = new Result(403,"权限不足请联系管理员",null);
String s = JSON.toJSONString(result);
WebUtils.renderString(httpServletResponse,s);
}
}
1)添加pom.xml依赖
io.springfox
springfox-swagger2
2.7.0
io.springfox
springfox-swagger-ui
2.7.0
com.github.xiaoymin
knife4j-spring-boot-starter
2.0.7
2)创建swagger配置文件
package com.ds.book.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.*;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.contexts.SecurityContext;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import java.util.ArrayList;
import java.util.List;
@Configuration
@EnableSwagger2
public class Swagger2Config {
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.pathMapping("/")
.apiInfo(apiInfo())
.select()
//swagger要扫描的包路径
.apis(RequestHandlerSelectors.basePackage("com.ds.book.controller"))
.paths(PathSelectors.any())
.build()
.securityContexts(securityContexts())
.securitySchemes(securitySchemes());
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder().title("图书管理系统接口文档")
//作者、路径和邮箱
.contact(new Contact("java大师","http://localhost:8080/doc.html","[email protected]"))
.version("1.0").description("图书管理接口文档").build();
}
private List securityContexts() {
//设置需要登录认证的路径
List result = new ArrayList<>();
result.add(getContextByPath("/.*"));
return result;
}
//通过pathRegex获取SecurityContext对象
private SecurityContext getContextByPath(String pathRegex) {
return SecurityContext.builder()
.securityReferences(defaultAuth())
.forPaths(PathSelectors.regex(pathRegex))
.build();
}
//默认为全局的SecurityReference对象
private List defaultAuth() {
List result = new ArrayList<>();
AuthorizationScope authorizationScope = new AuthorizationScope("global",
"accessEverything");
AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
authorizationScopes[0] = authorizationScope;
result.add(new SecurityReference("Authorization", authorizationScopes));
return result;
}
private List securitySchemes() {
//设置请求头信息
List result = new ArrayList<>();
//设置header中的token
ApiKey apiKey = new ApiKey("token", "token", "header");
result.add(apiKey);
return result;
}
}
3)修改SecurityConfig配置类,允许访问swagger的地址
//主要的配置文件,antMatchers匹配的路径,全部忽略,不进行JwtToken的认证
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers(
"/login",
"/logout",
"/css/**",
"/js/**",
"/index.html",
"favicon.ico",
"/doc.html",
"/webjars/**",
"/swagger-resources/**",
"/v2/api-docs/**"
);
}
4)编写LoginController接口,通过@Api和@ApiOperation注解使用swagger
package com.ds.book.controller;
import com.ds.book.entity.Result;
import com.ds.book.entity.User;
import com.ds.book.service.IUserService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Api(tags = "登录")
public class LoginController {
@Autowired
private IUserService userService;
@ApiOperation("登录")
@PostMapping("/login")
public Result login(@RequestBody User user){
return userService.login(user);
}
}
5)输入地址 http://localhost:8080/doc.html,进入swagger
6)点击登录进入登录接口,点击调试,发送
测试成功!
4.1 登录接口
注意:前后端分离项目,退出的时候,由前端清除浏览器请求header中的token和sessionStorage或者LocalStorage,后端只要返回一个退出成功的消息。
package com.ds.book.controller;
import com.ds.book.entity.Result;
import com.ds.book.entity.User;
import com.ds.book.service.IUserService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.security.Principal;
@RestController
@Api(tags = "登录")
public class LoginController {
@Autowired
private IUserService userService;
@Autowired
private UserDetailsService userDetailsService;
@ApiOperation("登录")
@PostMapping("/login")
public Result login(@RequestBody User user){
return userService.login(user);
}
@ApiOperation("退出")
@PostMapping("/logout")
public Result logout(){
return Result.success("退出成功");
}
@ApiOperation("获取当前登录用户信息")
@GetMapping("/user/info")
public User user(Principal principal){
if (principal == null){
return null;
}
String username = principal.getName();
User user = (User)userDetailsService.loadUserByUsername(username);
user.setPassword(null);
return user;
}
}
4.2菜单接口
package com.ds.book.controller;
import com.ds.book.entity.Menu;
import com.ds.book.entity.Result;
import com.ds.book.service.IMenuService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.models.auth.In;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
*
* 前端控制器
*
*
* @author java大师
* @since 2023-03-09
*/
@RestController
@Api(tags = "菜单管理")
public class MenuController {
@Autowired
private IMenuService menuService;
@GetMapping("/menus")
@ApiOperation("获取菜单树")
public Result getMenus(){
List
4.3用户接口
package com.ds.book.controller;
import com.ds.book.entity.Result;
import com.ds.book.entity.User;
import com.ds.book.service.IUserService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.models.auth.In;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.*;
import javax.jws.soap.SOAPBinding;
import java.util.List;
/**
*
* 前端控制器
*
*
* @author java大师
* @since 2023-03-09
*/
@RestController
@Api(tags = "用户管理")
public class UserController {
@Autowired
private IUserService userService;
@Autowired
private PasswordEncoder passwordEncoder;
@GetMapping("/users")
@ApiOperation("查询用户列表")
public Result getUsers(){
List list = userService.getUsers();
if (list != null){
return Result.success("查询成功",list);
}
return Result.error("查询失败");
}
@PostMapping("/user/add")
@ApiOperation("添加用户")
public Result addUser(@RequestBody User user){
user.setPassword(passwordEncoder.encode("123456"));
return userService.addUser(user);
}
@PostMapping("/user/update")
@ApiOperation("修改用户")
public Result updateUser(@RequestBody User user){
return userService.updateUser(user);
}
@PostMapping("/user/chooseRole/{userId}/{roleId}")
@ApiOperation("选择角色")
public Result chooseRole(@PathVariable Integer userId,@PathVariable Integer roleId){
return userService.chooseRole(userId,roleId);
}
@PostMapping("/user/delete/{id}")
@ApiOperation("删除用户")
public Result deleteUser(@PathVariable Integer id){
return userService.deleteUser(id);
}
}
4.4角色接口
package com.ds.book.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.ds.book.entity.Menu;
import com.ds.book.entity.Result;
import com.ds.book.entity.Role;
import com.ds.book.entity.RoleMenu;
import com.ds.book.mapper.RoleMapper;
import com.ds.book.mapper.RoleMenuMapper;
import com.ds.book.service.IRoleService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
/**
*
* 服务实现类
*
*
* @author java大师
* @since 2023-03-09
*/
@Service
public class RoleServiceImpl extends ServiceImpl implements IRoleService {
@Autowired
private RoleMapper roleMapper;
@Autowired
private RoleMenuMapper roleMenuMapper;
private List
vue create vue-book
选择Vue2,运行完毕,出现以下画面
执行绿色的命令,出现下列界面代表脚手架创建项目成功
//命令行安装
npm i element-ui -S
//main.js使用element-ui
import Vue from 'vue';
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import App from './App.vue';
Vue.use(ElementUI);
new Vue({
el: '#app',
render: h => h(App)
});
2.1安装依赖
npm install vue-router@3
2.2创建路由文件
import Vue from 'vue'
import VueRouter from "vue-router";
Vue.use(VueRouter)
//配置localhost:8080/跳转为登录页
const routes =[
{
path:'/',
name:'Login',
component:() => import('@/pages/Login.vue')
}
]
export default new VueRouter({
routes
})
4.1安装json-server
npm install -g json-server
4.2创建mock文件夹,新建db.json
{
"posts": [
{
"id": 1,
"title": "json-server",
"author": "typicode"
}
],
"users": [
{
"id": 1,
"username": "admin",
"password": "123"
}
],
"login":
{
"code": 200,
"message":"返回成功",
"data": {
"id": "1237361915165020161",
"username": "admin",
"phone": "111111111111",
"nickName": "javads",
"realName": "javads",
"sex": 1,
"deptId": "1237322421447561216",
"deptName": "测试部门",
"status": 1,
"email": "[email protected]",
"token":"ASDSADASDSW121DDSA",
"menus": [
{
"id": "1236916745927790564",
"title": "系统管理",
"icon": "el-icon-star-off",
"path": "/sys",
"name": "Sys",
"children": [
{
"id": "1236916745927790578",
"title": "角色管理",
"icon": "el-icon-s-promotion",
"path": "/sys/roles",
"name": "Roles",
"children": []
},
{
"id": "1236916745927790560",
"title": "菜单管理",
"icon": "el-icon-s-tools",
"path": "/sys/menus",
"name": "Menus",
"children": []
},
{
"id": "1236916745927790575",
"title": "用户管理",
"icon": "el-icon-s-custom",
"path": "/sys/users",
"name": "User",
"children": []
}
],
"spread": true,
"checked": false
},
{
"id": "1236916745927790569",
"title": "账号管理",
"icon": "el-icon-s-data",
"path": "/account",
"name": "Account",
"children": []
}
],
"permissions": [
"sys:log:delete",
"sys:user:add",
"sys:role:update",
"sys:dept:list"
]
}
},
"comments": [
{
"id": 1,
"body": "some comment",
"postId": 1
}
],
"profile": {
"name": "typicode"
}
}
4.3修改vue.config.js,json-server的默认端口为3000,将代理服务器的的端口改成3000
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
lintOnSave:false,
devServer:{
proxy:{
'/api':{
target:'http://localhost:3000',
pathRewrite:{'^/api':''},
ws:true, //不写为true,websocket
changeOrigin:true //不写为true
}
}
}
})
4.4修改package.json,在scripts添加以下代码
"mock": "json-server src/mock/db.json --port 3000 --middlewares src/mock/middlewares.js"
4.5 运行json-server,出现以下界面代表运行成功
json-server.cmd --watch db.jso
5.1配置axios请求拦截器,新建utils文件夹,新建api.js,输入以下内容
import router from '../router'
import axios from 'axios'
import {Message} from 'element-ui'
import {Loading} from 'element-ui'
axios.defaults.baseURL = '/api'
//添加遮罩层代码
let loading;
let loadingNum = 0;
//弹出遮罩层
function showLoading(){
if (loadingNum ===0){
loading = Loading.service({
lock:true,
text:'加载中,请稍后...',
background:'rgba(255,255,255,0.5)'
})
}
loadingNum++;
}
//关闭遮罩层
function hiddenLoading(){
loadingNum--;
if (loadingNum <=0){
loading.close();
}
}
/**
* 添加响应拦截器,在浏览器每次发请求之前,token放入http消息头当中
*/
axios.interceptors.request.use(config =>{
showLoading();
if(window.sessionStorage.getItem('token')){
config.headers.Authorization =window.sessionStorage.getItem('token')
}
console.log(config)
return config
},error => {
console.log(error)
})
/**
* 添加响应拦截器
*/
axios.interceptors.response.use(success => {
hiddenLoading();
if (success.status && success.status == 200){
if (success.data.code == 500 || success.data.code == 401 || success.data.code == 403) {
Message.error({
offset:200,
message:success.data.message
})
router.replace("/")
}
if (success.data.message){
Message.success({
offset:200,
message:success.data.message
})
}
}
return success.data
},error => {
hiddenLoading();
if (error.response.code == 504 || error.response.code == 404) {
Message.error({
message: '服务器跑路了'
});
} else if (error.response.status == 403) {
Message.error({
message: '权限不足,请联系管理员'
});
} else if (error.response.code == 401) {
Message.error({
message: '尚未登录,请先登录'
})
router.replace('/');
} else {
if (error.response.data.message) {
Message.error({
message: error.response.data.message
});
} else {
Message.error({
message: '未知错误'
});
}
}
return;
})
export default axios
5.2创建请求接口,新建http.js
import axios from './api'
export const login = (param) =>{
return axios.get(`/posts`, {param})
}
export const getUser = () =>{
return axios.get(`/users`, {})
}
6.1登录界面
欢迎登录
登录
取消
6.2处理后台请求返回工具类
export const initTmpRoutes = (menus) => {
let tmpRoutes = []
menus.forEach(menu => {
let {id,title,icon,path,name,children} = menu
if(children instanceof Array){
children = initTmpRoutes(children)
}
let tmpRoute = {
path:path,
meta:{icon:icon,title:title},
name:name,
children:children,
component:children.length?{render(c){return c('router-view')}}:()=>import(`@/pages${path}/${name}.vue`)
}
console.log('tmpRoute',tmpRoute.path)
tmpRoutes.push(tmpRoute)
})
return tmpRoutes
}
export const initRoutes = (menus)=>{
const homeRoute = {
path:'/home',
name:'Home',
meta:{title:'首页',icon: 'el-icon-star-off'},
component:() => import('@/pages/Home.vue'),
}
homeRoute.children = initTmpRoutes(menus);
console.log('homeRoute',homeRoute)
return homeRoute;
}
6.3首页、导航页和主页
home.vue
个人中心
设置
退出
底部
Nav.vue
{{ item.meta.title }}
{{ item.meta.title }}
RecursiveMenu.vue
{{ item.meta.title }}
{{ item.meta.title }}
可以看到左边的菜单和路由已经展示在浏览器中
注意:这里有一个坑,页面刷新以后,路由中的数据就会丢失,系统菜单会不显示
原因:页面刷新后,页面会重新实例化路由数据,因为是动态路由,所以页面刷新后会将router置为router/index.js配置的原始路由数据,所以匹配路由地址的时候会报错。
解决方法
思路:因为目前login接口返回的时候,直接将菜单数据传回前端,所以我们需要将菜单缓存起来,因为每次页面刷新vuex数据都会重置,所以不适合存储在vuex中,可以将菜单数据存储在sessionStorage中,页面刷新在实例化vue的created生命周期函数之前初始化路由即可
步骤
1)安装vuex
npm install vuex@3
2)修改登录页Login.vue
欢迎登录
登录
取消
3)创建store文件夹,创建index.js
import Vuex from 'vuex'
import Vue from "vue";
import {initRoutes} from "@/utils/routesUtil";
import Router from "@/router";
Vue.use(Vuex)
const state = {
token:window.sessionStorage.getItem('token')||'',
userData:window.sessionStorage.getItem('userData')||{},
routes:{}
}
const mutations = {
SETTOKEN(state,token){
window.sessionStorage.setItem('token',token)
state.token = token
},
SETUSERDATA(state,userData){
window.sessionStorage.setItem('userData',JSON.stringify(userData))
state.userData = userData
},
INITROUTES(state,menus){
let myRoutes = initRoutes(menus)
Router.options.routes = [myRoutes]
Router.addRoute(myRoutes);
state.routes = myRoutes
}
}
const actions = {
UPDATETOKEN(context,value){
context.commit('SETTOKEN',value)
},
UPDATEUSERDATA(context,value){
context.commit('SETUSERDATA',value)
}
}
const getters = {
userinfo(state){
return state.userData
},
menus(state){
return state.userData.menus
},
routes(state){
return state.routes.filter(item => {
return item.name==='Home'
})[0].children
}
}
export default new Vuex.Store({
state,
mutations,
actions,
getters
})
4)main.js修改
import Vue from 'vue'
import App from './App.vue'
import ElementUI from 'element-ui'
import router from './router'
import 'element-ui/lib/theme-chalk/index.css'
import store from "@/store"
Vue.config.productionTip = false
Vue.use(ElementUI)
//生成路由,由于没有获取菜单接口,所以直接从sessionStorage中直接去userData数据,进行路由的初始化
const init = async ()=>{
if (sessionStorage.getItem('token')){
if (store.state.routes){
await store.commit('INITROUTES',JSON.parse(sessionStorage.getItem('userData')))
}
}
}
//此处await不可缺少,需要等待路由数据先生成,才能进行vue实例的创建,否则会报错
async function call(){
await init();
new Vue({
render: h => h(App),
router,
store
}).$mount('#app')
}
call()
5)如果未登录,则跳转到login页处理,main.js添加如下内容
//路由导航守卫,每次路由地址改变前出发
router.beforeEach((to,from,next)=>{
if (sessionStorage.getItem('token')) {
next();
} else {
//如果是登录页面路径,就直接next()
if (to.path === '/login') {
next();
} else {
if(to.path === '/home'){
next();
}
next('/login');
}
}
})
安装e-icon-picker选择器