- 因为ElasticSearch更适合数据分析和检索的任务。MySQL的数据引擎支持全文索引,但是用模糊匹配可能导致索引失效,造成检索很慢。
- ElasticSearch支持更加复杂的查询,比如布尔查询,能够构建出满足各种需求的搜索功能。
开始用OCR转化了约100个标准的信息放到了Mysql数据库里,通过Java客户端批量导入到ES中。后面如果有增删改的话再同步增量导入到ES。这样其实会有业务的耦合,可能不太好。当时刚入门,按网上的教程做的,其实可以使用MQ实现异步的导入,或者使用Canal中间件监听MySQL的Binlog来导入。
首先,定义了两个自定义分词器,一个用于全文检索,tokenizer使用IK分词器,用拼音分词器过滤,还有一个就用于自动补全,tokenizer使用keywords,再用拼音分词器过滤。
再定义索引库Mapping的时候,将标准名字段设置全文检索的分词器用于创建索引,搜索用IK分词器。再新建一个字段用于自动补全,数据类型是completion,分词器是之前定义的用于自动补全的分词器。
然后在java代码中标准的实体类上添加一个属性suggestion,List类型。在导入数据的时候,将需要补全的字段放到suggestion里面去,导入到ES。
当前端键入标准名称时,会向后端发送ajax请求,服务端需要返回补全结果的集合。新建一个controller接受输入的前缀,调用相关的service,准备Request发起请求,并解析结果返回即可。
没有,这个项目很大,前期有一个数字化标准体系的构建,在体系构建里就已经梳理了当前建议参考的所有标准,并进行了层次化的分类。这个系统就是为了展示标准体系构建的成果,便于标准的查找检索。所有的住建标准加在一起可能才几百个,后期的更新也很少。单台服务器就可以满足数据搜索和数据存储的需求了。
JSON Web Token(JWT)是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准。允许我们使用JWT在用户和服务器之间传递安全可靠的信息。该token被设计为紧凑且安全的,特别适用于前后端无状态认证的场景。包含三个部分,之间用“.”隔开:
- 头部:该JWT最基本的信息,由Base64编码;
- 载荷:存放非敏感的有效信息,由Base64编码;
- 签证:由签名算法处理头部、载荷以及密钥。
项目本身基于一个简单的RBAC模型,RBAC模型由用户、角色、权限三个部分组成,通过定义角色的权限,并赋予用户角色来控制用户的权限。本项目主要分成两个角色,普通用户和管理员。
- 普通用户可以查询标准、浏览标准详情等。
- 管理员除了普通用户所具有的权限以外,还可以进行标准的管理、用户管理等。
SpringSecurity的本质就是一系列的过滤器:
- 【认证原理】其中,认证任务是由UsernamePasswordAuthenticationFilter实现的。在这个过滤器中,会将用户名和密码封装成Auhentication,然后调用认证管理器AuthenticationManager进行认证。认证管理器会进一步调用用户详情服务UserDetailService,根据用户名获取密码和权限信息,封装成UserDetails返回。认证管理器对比两个密码是否相同,如果相同就将权限信息加到Auhentication中并返回。
- 【认证过程的实现】所以,要自定义认证过程,只需要做两件事情:1)实现自己的UserDetailService,从数据库中动态加载用户名、密码、权限信息等。2)自定义认证过滤器,校验验证码→封装用户名和密码→调用管理器认证,在认证成功之后,将用户名称和权限生成JWT响应。最后将自定义的认证过滤器加到原来的认证过滤器之前就好了。
- 【授权原理】在认证过后,向请求方响应了Token信息,在请求方访问其他资源时,需要带着这个token到后台,后台需要一个授权过滤器获取Token信息,并解析用户权限,放在安全上下文中,方便其他的过滤器处理。
- 【授权过程的实现】具体的做法是自定义一个授权过滤器,获取请求头中的Token信息,如果没有直接放行由其他过滤器处理,如果Token错误,则直接返回,如果正确则解析出权限信息,组装成UsernamePasswodAuthentication放入安全上下文中。此外,还需要自定义权限拒绝处理器以及匿名用户拒绝处理器对无权访问或未登录的用户请求做出处理。
由于JWT是无状态的,服务器不存放用户登录信息,同一个账号多次登录都会颁发有效的JWT。如果需要只能由一个用户登录,可能需要其他的机制,例如,使用外部存储系统(例如Redis)记录用户的会话记录并设置过期时间,当有新的登录请求,可以从中了解到用户是否处于已经登录的状态,并做出处理。
-
自定义认证过滤器:继承
AbstractAuthenticationProcessingFilter
,重写方法attemptAuthentication
。- 解析
request
请求对象发送过来的数据流(用户名、密码和验证码); - 校验验证码,如果不正确直接写入
response
对象并返回; - 将用户名密码封装成票据,获取
AuthenticationManager
对象调用authenticate
方法。
public class JwtLoginAuthenticationFilter extends AbstractAuthenticationProcessingFilter { private RedisTemplate redisTemplate; /** * 通过setter方法注解redis模板对象 * @param redisTemplate */ public void setRedisTemplate(RedisTemplate redisTemplate) { this.redisTemplate = redisTemplate; } /** * 通过构造器传入自定义的登录地址 * @param loginUrl */ public JwtLoginAuthenticationFilter(String loginUrl) { super(loginUrl); } /** * 用户认证处理的方法 * @param request * @param response * @return * @throws AuthenticationException * @throws IOException * @throws ServletException * 我们约定请求方式必须是post方式,且请求的数据时json格式 * 约定请求是账户key:username 密码:password */ @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException { //判断请求方法必须是post提交,且提交的数据的内容必须是application/json格式的数据 if (!request.getMethod().equals("POST") || ! (request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE) || request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_UTF8_VALUE))) { throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod()); } //获取请求参数 //获取reqeust请求对象的发送过来的数据流 ServletInputStream in = request.getInputStream(); //将数据流中的数据反序列化成Map LoginReqVo vo = new ObjectMapper().readValue(in, LoginReqVo.class); response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.setCharacterEncoding("utf-8"); //1.判断参数是否合法 if (vo==null || StringUtils.isBlank(vo.getUsername()) || StringUtils.isBlank(vo.getPassword()) || StringUtils.isBlank(vo.getSessionId()) || StringUtils.isBlank(vo.getCode())) { R<Object> resp = R.error(ResponseCode.USERNAME_OR_PASSWORD_ERROR.getMessage()); response.getWriter().write(new ObjectMapper().writeValueAsString(resp)); return null; } //从程序执行的效率看,先进行校验码的校验,成本较低 String rCheckCode =(String) redisTemplate.opsForValue().get(StockConstant.CHECK_PREFIX + vo.getSessionId()); if (rCheckCode==null || ! rCheckCode.equalsIgnoreCase(vo.getCode())) { //响应验证码输入错误 R<Object> resp = R.error(ResponseCode.CHECK_CODE_ERROR.getMessage()); response.getWriter().write(new ObjectMapper().writeValueAsString(resp)); return null; } String username = vo.getUsername()trim(); String password = vo.getPassword(); //将用户名和密码信息封装到认证票据对象下 UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password); //调用认证管理器认证指定的票据对象 return this.getAuthenticationManager().authenticate(authRequest); } }
- 解析
-
自定义类实现
UserDetails
接口。@Data//setter getter toString @NoArgsConstructor @AllArgsConstructor @Builder public class LoginUserDetail implements UserDetails { /** * 用户名称 */ private String username; /** * 密码 */ private String password; /** * 权限信息 */ private List<GrantedAuthority> authorities; /** * 账户是否没有过期 */ private boolean isAccountNonExpired=true; /** * 账户是否被锁定 * true:没有被锁定 */ private boolean isAccountNonLocked=true; /** * 密码是否过期 * true:没有过期 */ private boolean isCredentialsNonExpired=true; /** * 账户是否禁用 * true:没有禁用 */ private boolean isEnabled=true; /** * 用户ID */ private String id; /** * 电话 */ private String phone; /** * 昵称 */ private String nickName; /** * 真实姓名 */ private String realName; /** * 性别(1.男 2.女) */ private Integer sex; /** * 账户状态(1.正常 2.锁定 ) */ private Integer status; /** * 邮箱 */ private String email; /** * 权限树,不包含按钮相关权限信息 */ private List<PermissionRespNodeVo> menus; /** * 按钮权限树 */ private List<String> permissions; }
-
自定义用户详情服务Bean:实现
UserDetailsService
接口,重写loadUserByUsername
方法,返回UserDetails
对象。- 根据用户名称获取数据库中用户的核心信息,不存在抛出异常;
- 组装
UserDetails
对象(用户名、密文密码、权限集合)。
@Service public class LoginUserDetailService implements UserDetailsService { @Autowired ...... @Override public UserDetails loadUserByUsername(String loginName) throws UsernameNotFoundException { // 根据用户名查询用户信息 SysUser dbUser= sysUserMapperExt.findUserByUserName(username); // 判断查询的用户信息 if (dbUser==null) { throw new UsernameNotFoundException("用户不存在"); } // 成功则返回用户的正常信息 // 获取指定用户的权限集合 添加获取侧边栏数据和按钮权限的结合信息 List<SysPermission> permissions = permissionService.getPermissionByUserId(dbUser.getId()); //前端需要的获取树状权限菜单数据 List<PermissionRespNodeVo> tree = permissionService.getTree(permissions, 0l, true); //前端需要的获取菜单按钮集合 List<String> authBtnPerms = permissions.stream() .filter(per -> !Strings.isNullOrEmpty(per.getCode()) && per.getType() == 3) .map(per -> per.getCode()).collect(Collectors.toList()); // 组装后端需要的权限标识 // 获取用户拥有的角色 List<SysRole> roles = sysRoleMapper.getRoleByUserId(dbUser.getId()); // 将用户的权限标识和角色标识维护到权限集合中 List<String> perms=new ArrayList<>(); permissions.stream().forEach(per->{ if (StringUtils.isNotBlank(per.getPerms())) { perms.add(per.getPerms()); } }); roles.stream().forEach(role->{ perms.add("ROLE_"+role.getName()); }); String[] permStr=perms.toArray(new String[perms.size()]); // 将用户权限标识转化成权限对象集合 List<GrantedAuthority> authorityList = AuthorityUtils.createAuthorityList(permStr); // 封装用户详情信息实体对象 LoginUserDetail loginUserDetail = new LoginUserDetail(); //将用户的id nickname等相同属性信息复制到详情对象中 BeanUtils.copyProperties(dbUser,loginUserDetail); loginUserDetail.setMenus(tree); loginUserDetail.setAuthorities(authorityList); loginUserDetail.setPermissions(authBtnPerms); return loginUserDetail; } }
-
认证成功后响应,生成JWT票据。
public class JwtLoginAuthenticationFilter extends AbstractAuthenticationProcessingFilter { //省略N行代码...... /** * 认证成功后,回调的方法 */ @Override protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { //获取用户的详情信息 LoginUserDetail userDetail = (LoginUserDetail) authResult.getPrincipal(); //组装LoginRespVoExt //获取用户名称 String username = userDetail.getUsername(); //获取权限集合对象 List<GrantedAuthority> authorities = userDetail.getAuthorities(); String auStrList = authorities.toString(); //复制userDetail属性值到LoginRespVoExt对象即可 LoginRespVoExt resp = new LoginRespVoExt(); BeanUtils.copyProperties(userDetail,resp); //生成token字符串:将用户名称和权限信息价格生成token字符串 String tokenStr = JwtTokenUtil.createToken(username, auStrList); resp.setAccessToken(tokenStr); //封装统一响应结果 R<Object> r = R.ok(resp); String respStr = new ObjectMapper().writeValueAsString(r); response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.setCharacterEncoding("UTF-8"); response.getWriter().write(respStr); } /** * 认证失败后,回调的方法 */ @Override protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { R<Object> r = R.error(ResponseCode.SYSTEM_PASSWORD_ERROR); String respStr = new ObjectMapper().writeValueAsString(r); response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.setCharacterEncoding("UTF-8"); response.getWriter().write(respStr); }
-
配置。
@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private RedisTemplate redisTemplate; /** * 定义公共的无需被拦截的资源 * @return */ private String[] getPubPath(){ //公共访问资源 String[] urls = { "/**/*.css","/**/*.js","/favicon.ico","/doc.html", "/druid/**","/webjars/**","/v2/api-docs","/api/captcha", "/swagger/**","/swagger-resources/**","/swagger-ui.html" }; return urls; } /** * 配置过滤规则 * @param http * @throws Exception */ @Override protected void configure(HttpSecurity http) throws Exception { //登出功能 http.logout().logoutUrl("/api/logout").invalidateHttpSession(true); //开启允许iframe 嵌套。security默认禁用ifram跨域与缓存 http.headers().frameOptions().disable().cacheControl().disable(); //session禁用 http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); http.csrf().disable();//禁用跨站请求伪造 http.authorizeRequests()//对资源进行认证处理 .antMatchers(getPubPath()).permitAll()//公共资源都允许访问 .anyRequest().authenticated(); //除了上述资源外,其它资源,只有 认证通过后,才能有权访问 //自定义的过滤器 http.addFilterBefore(jwtLoginAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); } /** * 自定义认证过滤器bean * @return * @throws Exception */ @Bean public JwtLoginAuthenticationFilter jwtLoginAuthenticationFilter() throws Exception { JwtLoginAuthenticationFilter filter = new JwtLoginAuthenticationFilter("/api/login"); filter.setAuthenticationManager(authenticationManagerBean()); filter.setRedisTemplate(redisTemplate); return filter; } /** * 定义密码加密器,实现对明文密码的加密和匹配操作 * @return */ @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } }
-
自定义授权过滤器
/** * @Description 定义授权过滤器,核心作用: * 1.过滤请求,获取请求头中的token字符串 * 2.解析token字符串,并获取token中信息:username role * 3.将用户名和权限信息封装到UsernamePassowrdAuthentionToken票据对象下 * 4.将票据对象放入安全上下文,方便校验权限时,随时获取 */ public class JwtAuthorizationFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { //1.从request对象下获取token数据,约定key:Authorization String tokenStr = request.getHeader(StockConstant.TOKEN_HEADER); //判断token字符串是否存在 if (tokenStr==null) { //如果票据为null,可能用户还没有认证,正准备去认证,所以放行请求 //放行后,会不会访问当受保护的资源呢?不会,因为没有生成UsernamePassowrdAuthentionToken filterChain.doFilter(request,response); return; } //2.解析tokenStr,获取用户详情信息 Claims claims = JwtTokenUtil.checkJWT(tokenStr); //token字符串失效的情况 if (claims==null) { //说明 票据解析出现异常,票据就失效了 R<Object> r = R.error(ResponseCode.TOKEN_NO_AVAIL); String respStr = new ObjectMapper().writeValueAsString(r); response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.setCharacterEncoding("UTF-8"); response.getWriter().write(respStr); return; } //获取用户名和权限信息 String userName = JwtTokenUtil.getUsername(tokenStr); //生成token时,权限字符串的格式是:[P8,ROLE_ADMIN] String perms = JwtTokenUtil.getUserRole(tokenStr); //生成权限集合对象 //P8,ROLE_ADMIN String strip = StringUtils.strip(perms, "[]"); List<GrantedAuthority> authorityList = AuthorityUtils.commaSeparatedStringToAuthorityList(strip); //将用户名和权限信息封装到token对象下 UsernamePasswordAuthenticationToken token=new UsernamePasswordAuthenticationToken(userName,null,authorityList); //将token对象存入安全上限文,这样,线程无论走到哪里,都可以获取token对象,验证当前用户访问对应资源是否被授权 SecurityContextHolder.getContext().setAuthentication(token); //资源发行 filterChain.doFilter(request,response); } }
-
配置授权过滤器
@Override protected void configure(HttpSecurity http) throws Exception { //......省略 //配置授权过滤器,过滤一切资源 http.addFilterBefore(jwtAuthorizationFilter(),JwtLoginAuthenticationFilter.class); } /** * 自定义授权过滤器 * @return */ @Bean public JwtAuthorizationFilter jwtAuthorizationFilter(){ return new JwtAuthorizationFilter(); }