基于SpringBoot + MyBatis的前后端分离实现在线办公系统

Posted Serendipity sn

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了基于SpringBoot + MyBatis的前后端分离实现在线办公系统相关的知识,希望对你有一定的参考价值。

在线办公系统

目录

1.开发环境的搭建及项目介绍

本项目目的是实现中小型企业的在线办公系统,云E办在线办公系统是一个用来管理日常的办公事务的一个系统

使用SpringSecurity做安全认证及权限管理,Redis做缓存,RabbitMq做邮件的发送,使用EasyPOI实现对员工数据的导入和导出,使用WebSocket做在线聊天

使用验证码登录

页面展示:

  1. 添加依赖

  2. 使用MyBatis的AutoGenerator自动生成mapper,service,Controller

2.登录模块及配置框架搭建

<1>Jwt工具类及对Token的处理

1.1根据用户信息生成Token

  1. 定义JWT负载中用户名的Key以及创建时间的Key

//用户名的key
private static final String CLAIM_KEY_USERNAME="sub";
//签名的时间
private static final String CLAIM_KEY_CREATED="created";
  1. 从配置文件中拿到Jwt的密钥和失效时间

/**
 * @Value的值有两类:
 * ① ${ property : default_value }
 * ② #{ obj.property? :default_value }
 * 第一个注入的是外部配置文件对应的property,第二个则是SpEL表达式对应的内容。 那个
 * default_value,就是前面的值为空时的默认值。注意二者的不同,#{}里面那个obj代表对象。
 */
//JWT密钥
@Value("${jwt.secret}")
private  String secret;

//JWT失效时间
@Value("${jwt.expiration}")
private Long expiration;
  1. 根据用户信息UserDetials生成Token

/**
 * 根据用户信息生成Token
 * @param userDetails
 * @return
 */
public String generateToken(UserDetails userDetails){
    //荷载
    Map<String,Object> claim=new HashMap<>();
    claim.put(CLAIM_KEY_USERNAME,userDetails.getUsername());
    claim.put(CLAIM_KEY_CREATED,new Date());
    return generateToken(claim);
}

/**
 * 根据负载生成JWT Token
 * @param claims
 * @return
 */
private String generateToken(Map<String,Object> claims) {
    return Jwts.builder()
            .setClaims(claims)
            .setExpiration(generateExpirationDate())//添加失效时间
            .signWith(SignatureAlgorithm.HS512,secret)//添加密钥以及加密方式
            .compact();
}

/**
 * 生成Token失效时间  当前时间+配置的失效时间
 * @return
 */
private Date generateExpirationDate() {
    return new Date(System.currentTimeMillis()+expiration*1000);
}

1.2根据Token生成用户名

/**
 * 根据Token生成用户名
 * @param token
 * @return
 */
public String getUsernameFormToken(String token){
    String username;
    //根据Token去拿荷载
    try {
        Claims claim=getClaimFromToken(token);
        username=claim.getSubject();//获取用户名
    } catch (Exception e) {
        e.printStackTrace();
        username=null;
    }
    return username;
}

/**
 * 从Token中获取荷载
 * @param token
 * @return
 */
private Claims getClaimFromToken(String token) {
    Claims claims=null;
    try {
        claims=Jwts.parser()
                .setSigningKey(secret)
                .parseClaimsJws(token)
                .getBody();
    } catch (Exception e) {
        e.printStackTrace();
    }
    return claims;
}

1.3判断Token是否有效

/**
 * 判断Token是否有效
 * Token是否过期
 * Token中的username和UserDetails中的username是否一致
 * @param token
 * @param userDetails
 * @return
 */
public boolean TokenIsValid(String token,UserDetails userDetails){
    String username = getUsernameFormToken(token);
    return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
}

/**
 * 判断Token是否过期
 * @param token
 * @return
 */
private boolean isTokenExpired(String token) {
    //获取Token的失效时间
    Date expireDate=getExpiredDateFromToken(token);
    //在当前时间之前,则失效
    return expireDate.before(new Date());
}

/**
 * 获取Token的失效时间
 * @param token
 * @return
 */
private Date getExpiredDateFromToken(String token) {
    Claims claims = getClaimFromToken(token);
    return claims.getExpiration();
}

1.4判断Token是否可以被刷新

/**
 * 判断token是否可用被刷新
 * 如果已经过期了,则可用被刷新,未过期,则不可用被刷新
 * @param token
 * @return
 */
public boolean canRefresh(String token){
    return !isTokenExpired(token);
}

1.5刷新Token,获取新的Token

/**
 * 刷新Token
 * @param token
 * @return
 */
public String refreshToken(String token){
    Claims claims=getClaimFromToken(token);
    claims.put(CLAIM_KEY_CREATED,new Date());
    return generateToken(claims);
}

<2>登录功能的实现

  • Controller层

    @ApiOperation(value = "登录之后返回token")
    @PostMapping("/login")
    //AdminLoginParam 自定义登录时传入的对象,包含账号,密码,验证码 
    public RespBean login(@RequestBody AdminLoginParam adminLoginParam, HttpServletRequest request){
        return adminService.login(adminLoginParam.getUsername(),adminLoginParam.getPassword(),adminLoginParam.getCode(),request);
    }
    
  • Service层

    /**
     * 登录之后返回token
     * @param username
     * @param password
     * @param request
     * @return
     */
    @Override
    public RespBean login(String username, String password,String code, HttpServletRequest request) {
        String captcha = (String)request.getSession().getAttribute("captcha");//验证码功能,后面提到
        //验证码为空或匹配不上
        if((code == null || code.length()==0) || !captcha.equalsIgnoreCase(code)){
            return RespBean.error("验证码错误,请重新输入");
        }
    
        //通过username在数据库查出这个对象
        //在SecurityConfig配置文件中,重写了loadUserByUsername方法,返回了userDetailsService Bean对象,使用我们自己的登录逻辑
        UserDetails userDetails = userDetailsService.loadUserByUsername(username);
        //如果userDetails为空或userDetails中的密码和传入的密码不相同
        if (userDetails == null||!passwordEncoder.matches(password,userDetails.getPassword())){
            return RespBean.error("用户名或密码不正确");
        }
        //判断账号是否可用
        if(!userDetails.isEnabled()){
            return RespBean.error("该账号已经被禁用,请联系管理员");
        }
    
        //更新登录用户对象,放入security全局中,密码不放
        UsernamePasswordAuthenticationToken authenticationToken=new UsernamePasswordAuthenticationToken(userDetails,null,userDetails.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
    
        //生成token
        String token = jwtTokenUtil.generateToken(userDetails);
        Map<String,String> tokenMap=new HashMap<>();
        tokenMap.put("token",token);
        tokenMap.put("tokenHead",tokenHead);//tokenHead,从配置文件yml中拿到的token的请求头 == Authorization
        return RespBean.success("登陆成功",tokenMap);//将Token返回
    }
    

<3>退出登录

退出登录功能由前端实现,我们只需要返回一个成功信息即可

@ApiOperation(value = "退出登录")
@PostMapping("/logout")
/**
 * 退出登录
 */
public RespBean logout(){
    return RespBean.success("注销成功");
}

<4>获取当前登录用户信息

  • Controller层

     @ApiOperation(value = "获取当前登录用户的信息")
        @GetMapping("/admin/info")
        public Admin getAdminInfo(Principal principal){
            //可通过principal对象获取当前登录对象
            if(principal == null){
                return null;
            }
            //当前用户的用户名
            String username = principal.getName();
            Admin admin= adminService.getAdminByUsername(username);
            //不能返回前端用户密码,设置为空
            admin.setPassword(null);
            //将用户角色返回
            admin.setRoles(adminService.getRoles(admin.getId()));
            return admin;
        }
    

<5>SpringSecurity的配置类SecurityConfig

5.1 覆盖SpringSecurity默认生成的账号密码,并让他走我们自定义的登录逻辑

//让SpringSecurity走我们自己登陆的UserDetailsService逻辑

//认证信息的管理 用户的存储 这里配置的用户信息会覆盖掉SpringSecurity默认生成的账号密码
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder());
}
//密码加解密
@Bean
public PasswordEncoder passwordEncoder(){
    return new BCryptPasswordEncoder();
}
@Override
@Bean  //注入到IOC中,在登录时使用到的userDetailsService就是这个Bean,loadUserByUsername方法是这里重写过的
public UserDetailsService userDetailsService(){
    return username->{
        Admin admin=adminService.getAdminByUsername(username);
        if(admin != null){
            admin.setRoles(adminService.getRoles(admin.getId()));
            return admin;
        }
        throw new UsernameNotFoundException("用户名或密码错误");
    };
}

登录功能中使用的userDetailsService对象由这里注入,重写loadUserByUsername方法实现自定义登录逻辑

5.2进行资源的拦截,权限设置,登录过滤器设置

@Override
protected void configure(HttpSecurity http) throws Exception {
    //使用Jwt不需要csrf
    http.csrf().disable()
            //基于token,不需要Session
            .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            //授权认证
            .authorizeRequests()
            .antMatchers("/doc.html").permitAll()
            //除了上面,所有的请求都要认证
            .anyRequest()
            .authenticated()
            .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                //动态权限配置
                @Override
                public <O extends FilterSecurityInterceptor> O postProcess(O o) {
                    o.setAccessDecisionManager(customUrlDecisionManager);
                    o.setSecurityMetadataSource(customFilter);
                    return o;
                }
            })
            .and()
            //禁用缓存
            .headers()
            .cacheControl();

    //添加jwt登录授权过滤器  判断是否登录
    http.addFilterBefore(jwtAuthencationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
    //添加自定义未授权和未登录结果返回
    http.exceptionHandling()
        //权限不足
            .accessDeniedHandler(restfulAccessDeniedHandler)
        //未登录
            .authenticationEntryPoint(restAuthorizationEntryPoint);

}

//将登录过滤器注入
@Bean
public JwtAuthencationTokenFilter jwtAuthencationTokenFilter(){
    return new JwtAuthencationTokenFilter();
}

//需要放行的资源
@Override
public void configure(WebSecurity web) throws Exception {
    web.ignoring().antMatchers(
            "/login",
            "/logout",
            "/css/**",
            "/js/**",
            //首页
            "/index.html",
            //网页图标
            "favicon.ico",
            //Swagger2
            "/doc.html",
            "/webjars/**",
            "/swagger-resources/**",
            "/v2/api-docs/**",
            //放行图像验证码
            "/captcha",
            //WebSocket
            "/ws/**"
    );
}
5.2.1登录过滤器的配置
public class JwtAuthencationTokenFilter extends OncePerRequestFilter {
   //Jwt存储头
    @Value("${jwt.tokenHeader}")
    private String tokenHeader;

    //Jwt头部信息
    @Value("${jwt.tokenHead}")
    private String tokenHead;

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        //token存储在Jwt的请求头中
        //通过key:tokenHeader拿到value:token

        //这里我们定义的token后期以:Bearer开头,空格分割,加上真正的jwt
        //通过tokenHeader(Authorization)拿到以Bearer开头 空格分割 加上真正的jwt的字符串
        String authHeader = httpServletRequest.getHeader(tokenHeader);

        //判断这个token的请求头是否为空且是以配置信息中要求的tokenHead开头
        if(authHeader != null && authHeader.startsWith(tokenHead)){
            //截取真正的jwt
            String authToken=authHeader.substring(tokenHead.length());
            String username=jwtTokenUtil.getUsernameFormToken(authToken);
            //token存在用户名但是未登录
            if(username != null && SecurityContextHolder.getContext().getAuthentication() == null){
                //登录
                UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                //验证token是否有效,重新设置用户对象
                if(jwtTokenUtil.TokenIsValid(authToken,userDetails)){
                    //把对象放到Security的全局中
                    UsernamePasswordAuthenticationToken authenticationToken=new UsernamePasswordAuthenticationToken(userDetails,null,userDetails.getAuthorities());
                    //将请求中的Session等信息放入Details,再放入Security全局中
                authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
                SecurityContextHolder.getContext().setAuthentication(authenticationToken);
                }

            }
        }
        //放行
        filterChain.doFilter(httpServletRequest,httpServletResponse);
    }
}
5.2.2添加未登录结果处理器

当未登录或者Token失效时访问未放行的接口时,自定义返回的结果

基于SpringBoot + MyBatis的前后端分离实现在线办公系统

一款小清新的 SpringBoot+ Mybatis 前后端分离后台管理系统项目

基于SpringBoot+MyBatis 五子棋双人对战

springboot+mybatis+vue+iviewui实现前后端分离的小Demo

基于springboot+vue的学生选课系统(前后端分离)

基于springboot+vue的房屋租赁系统(前后端分离)