点击关注公众号

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

本项目目的是实现中小型企业的在线办公系统,云E办在线办公系统是一个用来管理日常的办公事务的一个系统。
使用SpringSecurity做安全认证及权限管理,Redis做缓存,RabbitMq做邮件的发送,使用EasyPOI实现对员工数据的导入和导出,使用WebSocket做在线聊天。
使用验证码登录
页面展示:
  • 添加依赖
  • 使用MyBatis的AutoGenerator自动生成mapper,service,Controller

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

<1>Jwt工具类及对Token的处理
  • 根据用户信息生成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()
{

returnnew
 Date(System.currentTimeMillis()+expiration*
1000
);

}

  • 根据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;

}

  • 判断Token是否有效
/**

 * 判断Token是否有效

 * Token是否过期

 * Token中的username和UserDetails中的username是否一致

 * 
@param
 token

 * 
@param
 userDetails

 * 
@return
 */

publicbooleanTokenIsValid(String token,UserDetails userDetails)
{

    String username = getUsernameFormToken(token);

return
 username.equals(userDetails.getUsername()) && !isTokenExpired(token);

}


/**

 * 判断Token是否过期

 * 
@param
 token

 * 
@return
 */

privatebooleanisTokenExpired(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();

}

  • 判断Token是否可以被刷新
/**

 * 判断token是否可用被刷新

 * 如果已经过期了,则可用被刷新,未过期,则不可用被刷新

 * 
@param
 token

 * 
@return
 */

publicbooleancanRefresh(String token)
{

return
 !isTokenExpired(token);

}

  • 刷新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
){

returnnull
;

        }

//当前用户的用户名
        String username = principal.getName();

        Admin admin= adminService.getAdminByUsername(username);

//不能返回前端用户密码,设置为空
        admin.setPassword(
null
);

//将用户角色返回
        admin.setRoles(adminService.getRoles(admin.getId()));

return
 admin;

    }

<5>SpringSecurity的配置类SecurityConfig
覆盖SpringSecurity默认生成的账号密码,并让他走我们自定义的登录逻辑
//让SpringSecurity走我们自己登陆的UserDetailsService逻辑

//认证信息的管理 用户的存储 这里配置的用户信息会覆盖掉SpringSecurity默认生成的账号密码
@Override
protectedvoidconfigure(AuthenticationManagerBuilder auth)throws Exception 
{

    auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder());

}

//密码加解密
@Bean
public PasswordEncoder passwordEncoder()
{

returnnew
 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;

        }

thrownew
 UsernameNotFoundException(
"用户名或密码错误"
);

    };

}

登录功能中使用的userDetailsService对象由这里注入,重写loadUserByUsername方法实现自定义登录逻辑。
进行资源的拦截,权限设置,登录过滤器设置。
@Override
protectedvoidconfigure(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> 
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()
{

returnnew
 JwtAuthencationTokenFilter();

}


//需要放行的资源
@Override
publicvoidconfigure(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/**"
    );

}

登录过滤器的配置
publicclassJwtAuthencationTokenFilterextendsOncePerRequestFilter
{

//Jwt存储头
@Value
(
"${jwt.tokenHeader}"
)

private
 String tokenHeader;


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

private
 String tokenHead;


@Autowired
private
 JwtTokenUtil jwtTokenUtil;


@Autowired
private
 UserDetailsService userDetailsService;


@Override
protectedvoiddoFilterInternal(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);

    }

}

添加未登录结果处理器
当未登录或者Token失效时访问未放行的接口时,自定义返回的结果
@Component
publicclassRestAuthorizationEntryPointimplementsAuthenticationEntryPoint
{

@Override
publicvoidcommence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e)throws IOException, ServletException 
{

        httpServletResponse.setCharacterEncoding(
"UTF-8"
);

        httpServletResponse.setContentType(
"application/json"
);

        PrintWriter out = httpServletResponse.getWriter();

        RespBean bean=RespBean.error(
"尚未登录,请登录"
);

        bean.setCode(
401
);

        out.write(
new
 ObjectMapper().writeValueAsString(bean));

        out.flush();

        out.close();

    }

}

添加权限不足结果处理器
当访问接口没有权限时,自定义返回结果
@Component
publicclassRestfulAccessDeniedHandlerimplementsAccessDeniedHandler
{

@Override
publicvoidhandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e)throws IOException, ServletException 
{

        httpServletResponse.setCharacterEncoding(
"UTF-8"
);

        httpServletResponse.setContentType(
"application/json"
);

        PrintWriter out = httpServletResponse.getWriter();

        RespBean bean=RespBean.success(
"权限不足,请联系管理员"
);

        bean.setCode(
401
);

        out.write(
new
 ObjectMapper().writeValueAsString(bean));

        out.flush();

        out.close();

    }

}

添加权限控制器,根据请求的URL确定访问该URL需要什么角色
@Component
publicclassCustomFilterimplementsFilterInvocationSecurityMetadataSource
{


@Autowired
private
 IMenuService menuService;


    AntPathMatcher antPathMatcher=
new
 AntPathMatcher();


@Override
public Collection<ConfigAttribute> getAttributes(Object o)throws IllegalArgumentException 
{

//获取请求的URL
        String requestUrl = ((FilterInvocation) o).getRequestUrl();

        List<Menu> menus = menuService.getMenuWithRole();

//将URL所需要的角色放入Menu中
for
 (Menu menu:menus) {

//判断请求Url与菜单角色拥有的url是否匹配
if
(antPathMatcher.match(menu.getUrl(),requestUrl)){

// 该Url所需要的角色
                String[] str = menu.getRoles().stream().map(Role::getName).toArray(String[]::
new
);

//如果匹配上放入配置中,需要的角色
return
 SecurityConfig.createList(str);

            }

        }

//没匹配的url默认登录即可访问
return
 SecurityConfig.createList(
"ROLE_LOGIN"
);

    }


@Override
public Collection<ConfigAttribute> getAllConfigAttributes()
{

returnnull
;

    }


@Override
publicbooleansupports(Class<?> aClass)
{

returnfalse
;

    }

}

添加权限控制器,对角色信息进行处理,是否可用访问URL
@Component
publicclassCustomUrlDecisionManagerimplementsAccessDecisionManager
{

@Autowired
private
 CustomFilter customFilter;

@Override
publicvoiddecide(Authentication authentication, Object o, Collection<ConfigAttribute> collection)throws AccessDeniedException, InsufficientAuthenticationException 
{

for
 (ConfigAttribute configAttribute: collection) {

// 当前url所需要的角色
            List<ConfigAttribute> list= (List<ConfigAttribute>) customFilter.getAttributes(o);

            String[] needRoles=
new
 String[list.size()];

for
 (
int
 i = 
0
; i <list.size() ; i++) {

                needRoles[i]=list.get(i).getAttribute();

            }

//判断角色是否登录即可访问的角色,此角色在CustomFilter中设置

for
 (String needRole:needRoles) {

if
 (
"ROLE_LOGIN"
.equals((needRole))) {

//判断是否已经登录
if
(authentication 
instanceof
 AnonymousAuthenticationToken){

thrownew
 AccessDeniedException(
"尚未登录,请登录"
);

                    }
else
 {

return
;

                    }

                }

            }

//判断用户角色是否为url所需要的角色
//得到用户拥有的角色  这里在Admin类中已经将用户的角色放入了
            Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();

for
 (String needRole:needRoles) {

for
 (GrantedAuthority authority: authorities) {

if
(authority.getAuthority().equals(needRole)){

return
;

                    }

                }

            }

thrownew
 AccessDeniedException(
"权限不足,请联系管理员"
);

        }

    }


@Override
publicbooleansupports(ConfigAttribute configAttribute)
{

returnfalse
;

    }


@Override
publicbooleansupports(Class<?> aClass)
{

returnfalse
;

    }

}

<6>Swagger2的配置
@Configuration
@EnableSwagger
2

publicclassSwagger2Config
{


@Bean
public Docket createRestApi()
{

returnnew
 Docket(DocumentationType.SWAGGER_2)

//基础设置
                .apiInfo(apiInfo())

//扫描哪个包
                .select()

                .apis(RequestHandlerSelectors.basePackage(
"org.example.server.controller"
))

//任何路径都可以
                .paths(PathSelectors.any())

                .build()

                .securityContexts(securityContexts())

                .securitySchemes(securitySchemes());

    }


private ApiInfo apiInfo()
{

returnnew
 ApiInfoBuilder()

                .title(
"云E办接口文档"
)

                .description(
"云E办接口文档"
)

                .contact(
new
 Contact(
"朱云飞"
"http:localhost:8081/doc.html"
,
"[email protected]"
))

                .version(
"1.0"
)

                .build();


    }


private List<ApiKey> securitySchemes()
{

//设置请求头信息
        List<ApiKey> result=
new
 ArrayList<>();

        ApiKey apiKey=
new
 ApiKey(
"Authorization"
"Authorization"
,
"Header"
);

        result.add(apiKey);

return
 result;

    }


private List<SecurityContext> securityContexts()
{

//设置需要登录认证的路径
        List<SecurityContext> result=
new
 ArrayList<>();

        result.add(getContextByPath(
"/hello/.*"
));

return
 result;

    }


private SecurityContext getContextByPath(String pathRegex)
{

return
 SecurityContext.builder()

                .securityReferences(defaultAuth())
//添加全局认证
                .forPaths(PathSelectors.regex(pathRegex)) 
//带有pathRegex字段的接口访问不带添加的Authorization全局变量
                .build();

    }


//添加Swagger全局的Authorization  全局认证    固定的代码
private List<SecurityReference> defaultAuth()
{

        List<SecurityReference> result=
new
 ArrayList<>();

//设置范围为全局
        AuthorizationScope authorizationScope=
new
 AuthorizationScope(
"global"
,
"accessEeverything"
);

        AuthorizationScope[]authorizationScopes=
new
 AuthorizationScope[
1
];

        authorizationScopes[
0
]=authorizationScope;

        result.add((
new
 SecurityReference(
"Authorization"
,authorizationScopes)));
//这里的Authorization和上文ApiKey第二个参数一致
return
  result;

    }

}

注意:
 ApiKey apiKey=new ApiKey(
"Authorization"
"Authorization"
,
"Header"
);

<7>验证码功能(这里使用谷歌的验证码Captcha)
验证码的配置类
@Component
publicclassCaptchaConfig
{

@Bean
public DefaultKaptcha defaultKaptcha()
{

//验证码生成器
        DefaultKaptcha defaultKaptcha=
new
 DefaultKaptcha();

//配置
        Properties properties = 
new
 Properties();

//是否有边框
        properties.setProperty(
"kaptcha.border"
"yes"
);

//设置边框颜色
        properties.setProperty(
"kaptcha.border.color"
"105,179,90"
);

//边框粗细度,默认为1
// properties.setProperty("kaptcha.border.thickness","1");
//验证码
        properties.setProperty(
"kaptcha.session.key"
,
"code"
);

//验证码文本字符颜色 默认为黑色
        properties.setProperty(
"kaptcha.textproducer.font.color"
"blue"
);

//设置字体样式
        properties.setProperty(
"kaptcha.textproducer.font.names"
"宋体,楷体,微软雅黑"
);

//字体大小,默认40
        properties.setProperty(
"kaptcha.textproducer.font.size"
"30"
);

//验证码文本字符内容范围 默认为abced2345678gfynmnpwx
// properties.setProperty("kaptcha.textproducer.char.string", "");
//字符长度,默认为5
        properties.setProperty(
"kaptcha.textproducer.char.length"
"4"
);

//字符间距 默认为2
        properties.setProperty(
"kaptcha.textproducer.char.space"
"4"
);

//验证码图片宽度 默认为200
        properties.setProperty(
"kaptcha.image.width"
"100"
);

//验证码图片高度 默认为40
        properties.setProperty(
"kaptcha.image.height"
"40"
);

        Config config = 
new
 Config(properties);

        defaultKaptcha.setConfig(config);

return
 defaultKaptcha;

    }

}

验证码的控制器
@RestController
publicclassCaptchaController
{

@Autowired
private
 DefaultKaptcha defaultKaptcha;

@ApiOperation
(value = 
"验证码"
)

@GetMapping
(value = 
"/captcha"
,produces = 
"image/jpeg"
)

publicvoidcaptcha(HttpServletRequest request, HttpServletResponse response)
{

// 定义response输出类型为image/jpeg类型
        response.setDateHeader(
"Expires"
0
);

// Set standard HTTP/1.1 no-cache headers.
        response.setHeader(
"Cache-Control"
"no-store, no-cache, must-revalidate"
);

// Set IE extended HTTP/1.1 no-cache headers (use addHeader).
        response.addHeader(
"Cache-Control"
"post-check=0, pre-check=0"
);

// Set standard HTTP/1.0 no-cache header.
        response.setHeader(
"Pragma"
"no-cache"
);

// return a jpeg
        response.setContentType(
"image/jpeg"
);

//-------------------生成验证码 begin --------------------------
//获取验证码文本内容
        String text=defaultKaptcha.createText();

        System.out.println(
"验证码内容"
+text);

//将验证码文本内容放入Session
        request.getSession().setAttribute(
"captcha"
,text);

//根据文本验证码内容创建图形验证码
        BufferedImage image = defaultKaptcha.createImage(text);

        ServletOutputStream outputStream=
null
;

try
 {

             outputStream = response.getOutputStream();

//输出流输出图片,格式为jpg
            ImageIO.write(image, 
"jpg"
,outputStream);

            outputStream.flush();

        } 
catch
 (IOException e) {

            e.printStackTrace();

        }
finally
 {

if
(outputStream !=
null
){

try
 {

                    outputStream.close();

                } 
catch
 (IOException e) {

                    e.printStackTrace();

                }

            }

        }

//-------------------生成验证码 end --------------------------
    }

}

<8>根据用户ID查询用户所拥有操控权限的菜单列表
Controller层
@ApiOperation
(value = 
"通过用户ID查询菜单列表"
)

@GetMapping
(
"/menu"
)

public List<Menu> getMenuByAdminId()
{

return
 menuService.getMenuByAdminId();

}

Service层
@Override
public List<Menu> getMenuByAdminId()
{

//从Security全局上下文中获取当前登录用户Admin
    Admin admin= AdminUtil.getCurrentAdmin();

    Integer adminId=admin.getId();

    ValueOperations<String,Object> valueOperations = redisTemplate.opsForValue();

//从Redis获取菜单数据
    List<Menu> menus = (List<Menu>) valueOperations.get(
"menu_"
 + adminId);


//如果为空,从数据库中获取
if
(CollectionUtils.isEmpty(menus)){

        menus=menuMapper.getMenuByAdminId(adminId);

//查询之后放入Redis
        valueOperations.set(
"menu_"
+adminId,menus);

    }

return
 menus;

}

Mapper层
<!
-- 根据用户id查询菜单列表  -->
<
selectid
=
"getMenuByAdminId"
 resultMap=
"Menus"
>

SELECTDISTINCT
        m1.*,

        m2.id 
AS
 id2,

        m2.url 
AS
 url2,

        m2.path 
AS
 path2,

        m2.component 
AS
 component2,

        m2.
`name`AS
 name2,

        m2.iconCls 
AS
 iconCls2,

        m2.keepAlive 
AS
 keepAlive2,

        m2.requireAuth 
AS
 requireAuth2,

        m2.parentId 
AS
 parentId2,

        m2.enabled 
AS
 enabled2

FROM
        t_menu m1,

        t_menu m2,

        t_admin_role ar,

        t_menu_role mr

WHERE
        m1.id = m2.parentId

AND
 m2.id = mr.mid

AND
 mr.rid = ar.rid

AND
 ar.adminId = 
#{id}
AND
 m2.enabled = 
TRUE
ORDERBY
        m2.id

</
select
>

<9>使用Redis缓存根据用户ID查出来的菜单信息
Redis的配置类
@Configuration
publicclassRedisConfig
{

@Bean
public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)
{

        RedisTemplate<String,Object> redisTemplate=
new
 RedisTemplate<>();

//String类型Key序列器
        redisTemplate.setKeySerializer(
new
 StringRedisSerializer());

//String类型Value序列器
        redisTemplate.setValueSerializer(
new
 GenericJackson2JsonRedisSerializer());


//Hash类型的key序列器
        redisTemplate.setHashKeySerializer(
new
 StringRedisSerializer());

//Hash类型的Value序列器
        redisTemplate.setHashValueSerializer(
new
 GenericJackson2JsonRedisSerializer());


        redisTemplate.setConnectionFactory(redisConnectionFactory);

return
 redisTemplate;

    }

}

<10>全局异常的统一处理
@RestControllerAdvice
publicclassGlobalException
{

@ExceptionHandler
(SQLException
.
class
)

publicRespBeanrespBeanMysqlException
(
SQLExceptione
)
{

if
(e 
instanceof
 SQLIntegrityConstraintViolationException){

return
 RespBean.error(
"该数据有关联数据,操作失败"
);

        }

        e.printStackTrace();

return
 RespBean.error(
"数据库异常,操作失败"
);

    }


@ExceptionHandler
(DateException
.
class
)

publicRespBeanrespBeanDateException
(
DateExceptione
)
{

        e.printStackTrace();

return
 RespBean.error(e.getMessage());

    }


@ExceptionHandler
(Exception
.
class
)

publicRespBeanrespBeanException
(
Exceptione
)
{

        e.printStackTrace();

return
 RespBean.error(
"未知错误,请联系管理员"
);

    }

}

3.基础信息设置模块

职位,职称,权限组管理仅涉及单表的增删查改,这里不多写
<1>部门管理
获取所有部门
Mapper层:涉及父子类,递归查找
<select id=
"getAllDepartments"
 resultMap=
"DepartmentWithChildren"
>

    select

    <include refid=
"Base_Column_List"
/>

    from t_department

    where parentId=#{parentId}

</select>


<!-- 通用查询映射结果 -->

    <resultMap id=
"BaseResultMap"
 type=
"org.example.server.pojo.Department"
>

        <id column=
"id"
 property=
"id"
 />

        <result column=
"name"
 property=
"name"
 />

        <result column=
"parentId"
 property=
"parentId"
 />

        <result column=
"depPath"
 property=
"depPath"
 />

        <result column=
"enabled"
 property=
"enabled"
 />

        <result column=
"isParent"
 property=
"isParent"
 />

    </resultMap>


    <resultMap id=
"DepartmentWithChildren"
 type=
"org.example.server.pojo.Department"
 extends=
"BaseResultMap"
>

        <collection property=
"children"
 ofType=
"org.example.server.pojo.Department"
 select=
"org.example.server.mapper.DepartmentMapper.getAllDepartments"
        column=
"id"
>

        </collection>

    </resultMap>

    <!-- 通用查询结果列 -->

    <sql id=
"Base_Column_List"
>

        id, name, parentId, depPath, enabled, isParent

    </sql>

添加部门
<!--添加部门 -->

<!--statementType=
"CALLABLE 调用存储过程-->

<select id="
addDep
" statementType="
CALLABLE
">

    call addDep(#{name,mode=IN,jdbcType=VARCHAR},#{parentId,mode=IN,jdbcType=INTEGER},#{enabled,mode=IN,jdbcType=BOOLEAN},#{result,mode=OUT,jdbcType=INTEGER},#{id,mode=OUT,jdbcType=INTEGER})

</select>

删除部门
<!--添加部门 -->

<!--statementType=
"CALLABLE 调用存储过程-->

<select id="
addDep
" statementType="
CALLABLE
">

    call addDep(#{name,mode=IN,jdbcType=VARCHAR},#{parentId,mode=IN,jdbcType=INTEGER},#{enabled,mode=IN,jdbcType=BOOLEAN},#{result,mode=OUT,jdbcType=INTEGER},#{id,mode=OUT,jdbcType=INTEGER})

</select>

4.薪资模块及薪资管理模块

这里仅介绍获取全部操作员及操作员角色的更新,其他功能都是单表简单的增删查改
<1>获取全部操作员
Controller层
@ApiOperation
(value = 
"获取所有操作员"
)

@GetMapping
(
"/"
)

public List<Admin> getAllAdmins(String keywords)
{

return
 adminService.getAllAdmins(keywords);

}

Service层
/**

 * 获取所有操作员

 * 
@param
 keywords

 */

@Override
public List<Admin> getAllAdmins(String keywords)
{

//要传当前登录的Id,当前操作员不用查
return
 adminMapper.getAllAdmins(AdminUtil.getCurrentAdmin().getId(),keywords);

}

Mapper层
<!--获取所有操作员 -->

<select id=
"getAllAdmins"
 resultMap=
"AdminWithRole"
>

    SELECT

    a.*,

    r.id AS rid,

    r.`name` AS rname,

    r.nameZh AS rnameZh

    FROM

    t_admin a

    LEFT JOIN t_admin_role ar ON a.id = ar.adminId

    LEFT JOIN t_role r ON r.id = ar.rid

    WHERE

    a.id != #{id}

    <
if
 test=
"null!=keywords and ''!=keywords"
>

        AND a.`name` 
LIKE 
CONCAT'%', #{keywords}, '%' )
    </
if
>

    ORDER BY

    a.id

</select>

涉及操作员角色的查询
<2>操作员角色的修改
Service层:
/**

 * 更新操作员角色

 * 
@param
 adminId

 * 
@param
 rids

 * 
@return
 */

@Override
@Transactional
public RespBean updateAdminRole(Integer adminId, Integer[] rids)
{

//先将已经拥有的角色全部删除
    adminRoleMapper.delete(
new
 QueryWrapper<AdminRole>().eq(
"adminId"
,adminId));

//再将传过来的所有角色添加
    Integer result = adminRoleMapper.addAdminRole(adminId, rids);

if
(result == rids.length){

return
 RespBean.success(
"修改角色成功"
);

    }

return
 RespBean.error(
"更新角色失败"
);

}

思想:先将操作员所有的角色都删除,再将前端闯入的角色全部添加

5.员工模块管理

<1>分页获取全部员工信息
Controller
@ApiOperation
(value = 
"查询所有的员工(分页)"
)

@GetMapping
(
"/"
)

//beginDateScope入职的日期范围
public
 RespPageBean 
getEmployee(@RequestParam(defaultValue = "1")
 Integer currentPage,

                                @
RequestParam(defaultValue = "10")
 Integer size,

                                Employee employee,

                                LocalDate[] beginDateScope)
{




return
 employeeService.getEmployeeByPage(currentPage,size,employee,beginDateScope);

}

Service层
@Override
public RespPageBean getEmployeeByPage(Integer currentPage, Integer size, Employee employee, LocalDate[] beginDateScope)
{

    Page<Employee> page=
new
 Page<>(currentPage,size);

    IPage<Employee> iPage=employeeMapper.getEmployeeByPage(page,employee,beginDateScope);

    RespPageBean respPageBean=
new
 RespPageBean();

    respPageBean.setTotal(iPage.getTotal());

    respPageBean.setData(iPage.getRecords());

return
 respPageBean;

}

Mapper层
<resultMap id=
"EmployeeInfo"
 type=
"org.example.server.pojo.Employee"
 extends=
"BaseResultMap"
>

        <association property=
"nation"
 javaType=
"org.example.server.pojo.Nation"
>

            <id column=
"nid"
 property=
"id"
 />

            <result column=
"nname"
 property=
"name"
 />

        </association>

        <association property=
"politicsStatus"
 javaType=
"org.example.server.pojo.PoliticsStatus"
>

            <id column=
"pid"
 property=
"id"
 />

            <result column=
"pname"
 property=
"name"
 />

        </association>

        <association property=
"department"
 javaType=
"org.example.server.pojo.Department"
>

            <id column=
"did"
 property=
"id"
 />

            <result column=
"dname"
 property=
"name"
 />

        </association>

        <association property=
"joblevel"
 javaType=
"org.example.server.pojo.Joblevel"
>

            <id column=
"jid"
 property=
"id"
 />

            <result column=
"jname"
 property=
"name"
 />

        </association>

        <association property=
"position"
 javaType=
"org.example.server.pojo.Position"
>

            <id column=
"posid"
 property=
"id"
 />

            <result column=
"posname"
 property=
"name"
 />

        </association>

    </resultMap>


<!-- 将员工的政治面貌,职称,民族,职位,部门等信息填充进去 -->

<!-- 获取所有员工(分页) -->

<select id=
"getEmployeeByPage"
 resultMap=
"EmployeeInfo"
>

    SELECT

    e.*,

    n.id AS nid,

    n.`name` AS nname,

    p.id AS pid,

    p.`name` AS pname,

    d.id AS did,

    d.`name` AS dname,

    j.id AS jid,

    j.`name` AS jname,

    pos.id AS posid,

    pos.`name` AS posname

    FROM

    t_employee e,

    t_nation n,

    t_politics_status p,

    t_department d,

    t_joblevel j,

    t_position pos

    WHERE

    e.nationId = n.id

    AND e.politicId = p.id

    AND e.departmentId = d.id

    AND e.jobLevelId = j.id

    AND e.posId = pos.id

    <
if
 test=
"null!=employee.name and ''!=employee.name"
>

        AND e.`name` 
LIKE 
CONCAT'%', #{employee.name}, '%' )
    </
if
>

    <
if
 test
=
"null!=employee.politicId"
>

        AND e.politicId = #{employee.politicId}

    </
if
>

    <
if
 test=
"null!=employee.nationId"
>

        AND e.nationId = #{employee.nationId}

    </
if
>

    <
if
 test=
"null!=employee.jobLevelId"
>

        AND e.jobLevelId = #{employee.jobLevelId}

    </
if
>

    <
if
 test=
"null!=employee.posId"
>

        AND e.posId = #{employee.posId}

    </
if
>

    <
if
 test=
"null!=employee.engageForm and ''!=employee.engageForm"
>

        AND e.engageForm = #{employee.engageForm}

    </
if
>

    <
if
 test=
"null!=employee.departmentId"
>

        AND e.departmentId = #{employee.departmentId}

    </
if
>

    <
if
 test=
"null!=beginDateScope and 2==beginDateScope.length"
>

        AND e.beginDate BETWEEN #{beginDateScope[
0
]} AND #{beginDateScope[
1
]}

    </
if
>

    ORDER BY

    e.id

</select>

<2>使用EasyPOI对员工信息进行导入和导出
EasyPOI注解的使用
用于员工数据导入:Excel表中的部门,职称等字段在数据库员工表中找不到字段,数据库中是以id外键字段存储
员工数据的导出
@ApiOperation
(value = 
"导出员工数据"
)

@GetMapping
(value = 
"/export"
,produces = 
"application/octet-stream"
)

publicvoidexportEmployee(HttpServletResponse response)
{

    List<Employee> list = employeeService.getEmployee(
null
);

//参数:文件名,表名,导出的Excel的类型(03版本)
    ExportParams params=
new
 ExportParams(
"员工表"
,
"员工表"
, ExcelType.HSSF);

    Workbook workbook = ExcelExportUtil.exportExcel(params, Employee
.classlist)
;

//输入workbook
    ServletOutputStream out=
null
;

try
{

//流形式
        response.setHeader(
"content-type"
,
"application/octet-stream"
);

//防止中文乱码
        response.setHeader(
"content-disposition"
,
"attachment;filename="
+ URLEncoder.encode(
"员工表.xls"
,
"UTF-8"
));

        out = response.getOutputStream();

        workbook.write(out);

    }
catch
 (IOException e){

        e.printStackTrace();

    }
finally
 {

if
(out != 
null
){

try
 {

                out.close();

            } 
catch
 (IOException e) {

                e.printStackTrace();

            }

        }

    }

}

员工数据的导入
@ApiOperation
(value = 
"导入员工数据"
)

@PostMapping
(
"/import"
)

public RespBean importEmployee(MultipartFile file)
{

//准备导入的数据表
    ImportParams params=
new
 ImportParams();

//去掉第一行:标题行
    params.setTitleRows(
1
);

    List<Nation> nationList = nationService.list();

    List<PoliticsStatus> politicsStatusList=politicsStatusService.list();

    List<Department> departmentList=departmentService.list();

    List<Joblevel> joblevelList=joblevelService.list();

    List<Position> positionList=positionService.list();

try
 {

//将Excel表变为List
        List<Employee> list = ExcelImportUtil.importExcel(file.getInputStream(), Employee
.classparams)
;

        list.forEach(employee -> {

//获取民族ID
            Integer nationId = nationList.get(nationList.indexOf(
new
 Nation(employee.getNation().getName()))).getId();

            employee.setNationId(nationId);


//获取政治面貌Id
            Integer politicsStatusId=politicsStatusList.get(politicsStatusList.indexOf(
new
 PoliticsStatus(employee.getPoliticsStatus().getName()))).getId();

            employee.setPoliticId(politicsStatusId);


//获取部门Id
            Integer departmentId=departmentList.get(departmentList.indexOf(
new
 Department(employee.getDepartment().getName()))).getId();

            employee.setDepartmentId(departmentId);


//获取职称Id
            Integer joblevelId=joblevelList.get(joblevelList.indexOf(
new
 Joblevel(employee.getJoblevel().getName()))).getId();

            employee.setJobLevelId(joblevelId);


//获取职位Id
            Integer positionId=positionList.get(positionList.indexOf(
new
 Position(employee.getPosition().getName()))).getId();

            employee.setPosId(positionId);

        });


if
(employeeService.saveBatch(list)){

return
 RespBean.success(
"导入成功"
);

        }

    } 
catch
 (Exception e) {

        e.printStackTrace();

    }

return
 RespBean.error(
"导入失败"
);

}

<3>使用RabbitMQ对新入职的员工发送欢迎邮件
这里使用SMTP:需要先去邮箱开通SMTP服务
  • RabbitMQ消息发送的可靠性
消息落库,对消息状态进行标记
步骤:
  • 发送消息时,将当前消息数据存入数据库,投递状态为消息投递中
  • 开启消息确认回调机制。确认成功,更新投递状态为消息投递成功
  • 开启定时任务,重新投递失败的消息。重试超过3次,更新投递状态为投递失败
消息延迟投递,做二次确认,回调检查
步骤:
  • 发送消息时,将当前消息存入数据库,消息状态为消息投递
  • 过一段时间进行第二次的消息发送
  • 开启消息回调机制,当第一次发送的消息被成功消费时,消费端的确认会被MQ Broker监听,成功则将消息队列中的状态变为投递成功
  • 如果消息投递没有成功,则过一段时间第二次发送的消息也会被MQ Broker监听到,会根据这条消息的ID去消息数据库查找,如果发现消息数据库中的状态为投递中而不是投递成功,则会通知消息放松端重新进行步骤一
  • 消息功能的实现
在进行新员工插入成功后,对新员工发出邮件,并将发送的邮件保存到数据库中
//获取合同开始和结束的时间
    LocalDate beginContact=employee.getBeginContract();

    LocalDate endContact=employee.getEndContract();

long
 days = beginContact.until(endContact, ChronoUnit.DAYS);

//保留两位小数
    DecimalFormat decimalFormat=
new
 DecimalFormat(
"##.00"
);

    employee.setContractTerm(Double.parseDouble(decimalFormat.format(days/
365.00
)));

if
(employeeMapper.insert(employee) == 
1
){

//获取新插入的员工对象
        Employee emp=employeeMapper.getEmployee(employee.getId()).get(
0
);

//数据库记录发送的消息
        String msgId = UUID.randomUUID().toString();

        MailLog mailLog=
new
 MailLog();

        mailLog.setMsgId(msgId);

        mailLog.setEid(employee.getId());

        mailLog.setStatus(
0
);

//消息的状态保存在Model中
        mailLog.setRouteKey(MailConstants.MAIL_ROUTING_KEY_NAME);

        mailLog.setExchange(MailConstants.MAIL_EXCHANGE_NAME);

        mailLog.setCount(MailConstants.MAX_TRY_COUNT);

        mailLog.setTryTime(LocalDateTime.now().plusMinutes(MailConstants.MAX_TRY_COUNT));

        mailLog.setCreateTime(LocalDateTime.now());

        mailLog.setUpdateTime(LocalDateTime.now());

        mailLogMapper.insert(mailLog);


//发送信息
//发送交换机,路由键,用户对象和消息ID
        rabbitTemplate.convertAndSend(MailConstants.MAIL_EXCHANGE_NAME,

                MailConstants.MAIL_ROUTING_KEY_NAME,

                emp,

new
 CorrelationData(msgId));

return
 RespBean.success(
"添加成功"
);

    }

return
 RespBean.error(
"添加失败"
);

}

消费端的处理,这里我们使用上述第一种方式,—>消息落库,对消息状态进行标记. 为保证消费者不重复消费同一消息,采取 消息序号+我们传入的消息msgId来识别每一个消息
@Component
publicclassMailReceiver
{


//日志
privatestaticfinal
 Logger LOGGER = LoggerFactory.getLogger(MailReceiver
.class)
;


@Autowired
private
 JavaMailSender javaMailSender;

@Autowired
private
 MailProperties mailProperties;

@Autowired
private
 TemplateEngine templateEngine;

@Autowired
private
 RedisTemplate redisTemplate;


@RabbitListener
(queues = MailConstants.MAIL_QUEUE_NAME)

//拿取Message 和 channel 可以拿到 消息序号鉴别消息是否统一个消息多收    通过消息序号+msgId两个来鉴别
publicvoidhandler(Message message, Channel channel)
{

        Employee employee = (Employee) message.getPayload();

        MessageHeaders headers = message.getHeaders();

//消息序号
long
 tag = (
long
) headers.get(AmqpHeaders.DELIVERY_TAG);

//拿到存取的UUID
        String msgId = (String) headers.get(
"spring_returned_message_correlation"
);
//这个key固定
        HashOperations hashOperations = redisTemplate.opsForHash();

try
 {

//从Redis中拿取,如果存在,说明消息已经发送成功了,这里直接确认返回
if
 (hashOperations.entries(
"mail_log"
).containsKey(msgId)){

                LOGGER.error(
"消息已经被消费=============>{}"
,msgId);

/**

                 * 手动确认消息

                 * tag:消息序号

                 * multiple:是否确认多条

                 */

                channel.basicAck(tag,
false
);

return
;

            }

            MimeMessage msg = javaMailSender.createMimeMessage();

            MimeMessageHelper helper = 
new
 MimeMessageHelper(msg);

//发件人
            helper.setFrom(mailProperties.getUsername());

//收件人
            helper.setTo(employee.getEmail());

//主题
            helper.setSubject(
"入职欢迎邮件"
);

//发送日期
            helper.setSentDate(
new
 Date());

//邮件内容
            Context context = 
new
 Context();

//用于theymeleaf获取
            context.setVariable(
"name"
, employee.getName());

            context.setVariable(
"posName"
, employee.getPosition().getName());

            context.setVariable(
"joblevelName"
, employee.getJoblevel().getName());

            context.setVariable(
"departmentName"
, employee.getDepartment().getName());

//将准备好的theymeleaf模板中的信息转为String
            String mail = templateEngine.process(
"mail"
, context);

            helper.setText(mail, 
true
);

//发送邮件
            javaMailSender.send(msg);

            LOGGER.info(
"邮件发送成功"
);

//将消息id存入redis
//mail_log是Redis  hash的key   msgId是真正的key  "OK"是Value,主要是拿到msgId,"OK"没啥用
            hashOperations.put(
"mail_log"
, msgId, 
"OK"
);

//手动确认消息
            channel.basicAck(tag, 
false
);

        } 
catch
 (Exception e) {

/**

             * 手动确认消息

             * tag:消息序号

             * multiple:是否确认多条

             * requeue:是否退回到队列

             */

try
 {

                channel.basicNack(tag,
false
,
true
);

            } 
catch
 (IOException ex) {

                LOGGER.error(
"邮件发送失败=========>{}"
, e.getMessage());

            }

            LOGGER.error(
"邮件发送失败=========>{}"
, e.getMessage());

        }

    }

}

消息的配置类,确认应答等
@Configuration
publicclassRabbitMQConfig
{

privatestaticfinal
 Logger LOGGER = LoggerFactory.getLogger(RabbitMQConfig
.class)
;

@Autowired
private
 CachingConnectionFactory cachingConnectionFactory;


@Autowired
private
 IMailLogService mailLogService;


@Bean
public RabbitTemplate rabbitTemplate()
{

        RabbitTemplate rabbitTemplate = 
new
 RabbitTemplate(cachingConnectionFactory);


/**

         * 消息确认回调,确认消息是否到达broker

         * data:消息的唯一标识

         * ack:确认结果

         * cause:失败原因

         */

        rabbitTemplate.setConfirmCallback((data,ack,cause)->{

            String msgId = data.getId();

if
(ack){

                LOGGER.info(
"{}======>消息发送成功"
,msgId);

                mailLogService.update(
new
 UpdateWrapper<MailLog>().set(
"status"
,
1
 ).eq(
"msgId"
,msgId));

            }
else
 {

                LOGGER.error(
"{}=====>消息发送失败"
,msgId);

            }

        });


/**

         * 消息失败回调,比如router不到queue时回调

         * msg:消息的主题

         * repCode:响应码

         * repText:响应描述

         * exchange:交换机

         * routingkey:路由键

         */

        rabbitTemplate.setReturnCallback((msg,repCode,repText,exchange,routingkey)->{

            LOGGER.error(
"{}=====>消息发送queue时失败"
,msg.getBody());

        });

return
 rabbitTemplate;

    }


@Bean
public Queue queue()
{

returnnew
 Queue(MailConstants.MAIL_QUEUE_NAME);

    }


@Bean
public DirectExchange directExchange()
{

returnnew
 DirectExchange(MailConstants.MAIL_EXCHANGE_NAME);

    }


@Bean
public Binding binding()
{

return
 BindingBuilder.bind(queue()).to(directExchange()).with(MailConstants.MAIL_ROUTING_KEY_NAME);

    }

6.在线聊天功能的实现

这里使用WebSocket
WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。
WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。
在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。
WebSocket的配置
这里主要是前端实现,后端只是增加一些配置
@Configuration
@EnableWebSocketMessageBroker
publicclassWebSocketConfigimplementsWebSocketMessageBrokerConfigurer
{

@Value
(
"${jwt.tokenHead}"
)

private
 String tokenHead;

@Autowired
private
 JwtTokenUtil jwtTokenUtil;

@Autowired
private
 UserDetailsService userDetailsService;



/**

     * 添加这个Endpoint,这样在网页可以通过websocket连接上服务

     * 也就是我们配置websocket的服务地址,并且可以指定是否使用socketJS

     * 
@param
 registry

     */

@Override
publicvoidregisterStompEndpoints(StompEndpointRegistry registry)
{

/**

         * 1.将ws/ep路径注册为stomp的端点,用户连接了这个端点就可以进行websocket通讯,支持socketJS

         * 2.setAllowedOrigins("*"):允许跨域

         * 3.withSockJS():支持socketJS访问

         */

        registry.addEndpoint(
"/ws/ep"
).setAllowedOrigins(
"*"
).withSockJS();

    }



/**

     * 输入通道参数配置  JWT配置

     * 
@param
 registration

     */

@Override
publicvoidconfigureClientInboundChannel(ChannelRegistration registration)
{

        registration.interceptors(
new
 ChannelInterceptor() {

@Override
public
 Message<?> preSend(Message<?> message, MessageChannel channel) {

                StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor
.class)
;

//判断是否为连接,如果是,需要获取token,并且设置用户对象
if
 (StompCommand.CONNECT.equals(accessor.getCommand())){

//拿取Token
                    String token = accessor.getFirstNativeHeader(
"Auth-Token"
);
//参数前端已经固定
if
 (!StringUtils.isEmpty(token)){

                        String authToken = token.substring(tokenHead.length());

                        String username = jwtTokenUtil.getUsernameFormToken(authToken);

//token中存在用户名
if
 (!StringUtils.isEmpty(username)){

//登录
                            UserDetails userDetails = userDetailsService.loadUserByUsername(username);

//验证token是否有效,重新设置用户对象
if
 (jwtTokenUtil.TokenIsValid(authToken,userDetails)){

                                UsernamePasswordAuthenticationToken authenticationToken =

new
 UsernamePasswordAuthenticationToken(userDetails, 
null
,

                                                userDetails.getAuthorities());

                                SecurityContextHolder.getContext().setAuthentication(authenticationToken);

                                accessor.setUser(authenticationToken);

                            }

                        }

                    }

                }

return
 message;

            }

        });

    }


/**

     * 配置消息代理

     * 
@param
 registry

     */

@Override
publicvoidconfigureMessageBroker(MessageBrokerRegistry registry)
{

//配置代理域,可以配置多个,配置代理目的地前缀为/queue,可以在配置域上向客户端推送消息
        registry.enableSimpleBroker(
"/queue"
);

    }

}

作者:Serendipity  sn 链接:
blog.csdn.net/qq_45704528/article/details/119699269
刚刚整理好了的第五版《Java大厂面试题》,而且已经分类 25 PDF累计 2098页!
整理的面试题,内容列表
互联网大厂面试题,怎么领取?
 注意,不要乱回复 
(一定要回复 面试题)否则获取不了
在看 
继续阅读
阅读原文