springboot+springsecurity+jwt+elementui图书管理系统

分类:编程技术 时间:2024-02-20 19:15 浏览:0 评论:0
0

文章springboot+springsecurity+jwt+elementui图书管理系统,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

​​图书管理系统​​

一、springboot后台

1、mybatis-plus整合

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、springsecurity-jwt整合

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 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;@Componentpublic 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注解

@RestControllerpublic class HelloController {    @GetMapping("/hello")    @PreAuthorize("hasRole('ROLE_ADMIN')")    public String hello(){        return "hello";    }}
2.4.5.2 增强方式授权(数据库表配置)

1)创建我们自己的FilterInvocationSecurityMetadataSource,实现getAttributes方法,获取请求url所需要的角色

@Componentpublic 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 menus = menuService.getMenus();        for (Menu menu : menus) {            if (antPathMatcher.match(menu.getUrl(),requestURI)){                String[] roles = menu.getRoles().stream().map(role -> role.getRoleCode()).toArray(String[]::new);                return SecurityConfig.createList(roles);            }        }        return null;    }    @Override    public Collection getAllConfigAttributes() {        return null;    }    @Override    public boolean supports(Class clazz) {        return false;    }}

2)创建我们自己的决策管理器AccessDecisionManager,实现decide方法,判断步骤1)中获取到的角色和我们目前登录的角色是否相同,相同则允许访问,不相同则不允许访问,

@Componentpublic class MyAccessDecisionManager implements AccessDecisionManager {        //1、认证通过后,会往authentication中填充用户信息    //2、拿authentication中的权限与上一步获取到的角色信息进行比对,比对成功后,允许访问    @Override    public void decide(Authentication authentication, Object object, Collection configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {        Collection 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

@Configurationpublic 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方法

@Componentpublic 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方法

@Componentpublic 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);    }}

3、整合swagger2

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@EnableSwagger2public 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","fry000@qq.com"))                .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的认证@Overridepublic 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、业务接口

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 allMenus = menuService.getMenuTree(); return Result.success("查询成功",allMenus); } @PostMapping("/menu/add") @ApiOperation("添加菜单") public Result addMenu(@RequestBody Menu menu){ return menuService.addMenu(menu); } @PostMapping("/menu/update") @ApiOperation("修改菜单") public Result updateMenu(@RequestBody Menu menu){ return menuService.updateMenu(menu); } @PostMapping("/menu/delete/{id}") @ApiOperation("删除菜单") public Result deleteMenu(@PathVariable Integer id){ return menuService.deleteMenu(id); }}

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 */@Servicepublic class RoleServiceImpl extends ServiceImpl implements IRoleService { @Autowired private RoleMapper roleMapper; @Autowired private RoleMenuMapper roleMenuMapper; private List buildMenuTree(List menus, Integer parentId) { List treeMenus = new ArrayList<>(); for (Menu menu : menus) { if (parentId==0 ? menu.getParentId()==0 : parentId.equals(menu.getParentId())) { List children = buildMenuTree(menus, menu.getId()); if (!children.isEmpty()) { menu.setChildren(children); } treeMenus.add(menu); } } return treeMenus; } @Override public List getRoles() { List roles = roleMapper.getRoles(); for (Role role : roles) { role.setMenus(buildMenuTree(role.getMenus(),0)); } return roles; } @Override public Result chooseMenus(Integer roleId, Integer[] menuIds) { try { roleMenuMapper.delete(new QueryWrapper().eq("role_id",roleId)); for (Integer menuId : menuIds) { RoleMenu roleMenu = new RoleMenu(); roleMenu.setRoleId(roleId); roleMenu.setMenuId(menuId); roleMenuMapper.insert(roleMenu); } return Result.success("添加成功"); } catch (Exception exception) { return Result.error("添加失败"); } }}

二、springboot前端

1、vue-cli创建vue-book前端项目

vue create vue-book

选择Vue2,运行完毕,出现以下画面

执行绿色的命令,出现下列界面代表脚手架创建项目成功

2、整合elementui

//命令行安装npm i element-ui -S//main.js使用element-uiimport 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)});

3、安装vue-router

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、整合json-server

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": "xxxx@qq.com",      "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、整合axios

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、业务功能

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

RecursiveMenu.vue

可以看到左边的菜单和路由已经展示在浏览器中

注意:这里有一个坑,页面刷新以后,路由中的数据就会丢失,系统菜单会不显示

原因:页面刷新后,页面会重新实例化路由数据,因为是动态路由,所以页面刷新后会将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 = falseVue.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选择器



1. 本站所有资源来源于用户上传或网络,仅作为参考研究使用,如有侵权请邮件联系站长!
2. 本站积分货币获取途径以及用途的解读,想在本站混的好,请务必认真阅读!
3. 本站强烈打击盗版/破解等有损他人权益和违法作为,请各位会员支持正版!
4. 编程技术 > springboot+springsecurity+jwt+elementui图书管理系统

用户评论