基于SpringBoot + MyBatis的前后端分离实现在线办公系统
Posted Serendipity sn
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了基于SpringBoot + MyBatis的前后端分离实现在线办公系统相关的知识,希望对你有一定的参考价值。
在线办公系统
目录
1.开发环境的搭建及项目介绍
本项目目的是实现中小型企业的在线办公系统,云E办在线办公系统是一个用来管理日常的办公事务的一个系统
使用SpringSecurity做安全认证及权限管理,Redis做缓存,RabbitMq做邮件的发送,使用EasyPOI实现对员工数据的导入和导出,使用WebSocket做在线聊天
使用验证码登录
页面展示:
-
添加依赖
-
使用MyBatis的AutoGenerator自动生成mapper,service,Controller
2.登录模块及配置框架搭建
<1>Jwt工具类及对Token的处理
1.1根据用户信息生成Token
-
定义JWT负载中用户名的Key以及创建时间的Key
//用户名的key
private static final String CLAIM_KEY_USERNAME="sub";
//签名的时间
private static final String CLAIM_KEY_CREATED="created";
-
从配置文件中拿到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;
-
根据用户信息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+vue+iviewui实现前后端分离的小Demo