SpringSecurity单体应用

注:本文讲述的是Security在单体架构的应用,不支持集群跨域。另外,本文基于前后端不分离,使用的前端模板引擎是Thymeleaf。

一、导入Security依赖

第一个依赖是SpringBoot为Security提供的starter依赖,导入后,Security立即生效,会默认生成一个用户名和密码(项目重启后控制台可见),使项目中所有的请求都需要认证。
第二个依赖是thymeleaf模板引擎为支持Security提供的依赖,这个依赖其实不是必须的,下文会简单提一下它的用法。

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency><groupId>org.thymeleaf.extras</groupId><artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>

二、创建UserDetails对象

一般我们的项目中已经有了User对象,直接让它实现UserDetails接口即可。UserDetails是Security默认的用户信息存储媒介,它只存储用户名(username)、密码(password)、权限(authorities)和其他一些用户状态,具体如下:

public interface UserDetails extends Serializable {// 获取授予用户的权限,包括权限级别authority和级别角色roleCollection<? extends GrantedAuthority> getAuthorities();// 获取用户密码String getPassword();// 获取用户名String getUsername();// 标识用户的帐户是否已过期,true(未过期)、false(已过期),过期帐户无法验证。boolean isAccountNonExpired();// 标识用户是被锁定还是未锁定,true(未锁定),false(锁定),锁定的用户无法进行身份验证。boolean isAccountNonLocked();// 标识用户的凭据(密码)是否已过期,true(未过期)、false(已过期),过期的凭据会阻止身份验证。boolean isCredentialsNonExpired();// 标识用户是启用还是禁用,true(启用),false(禁用),禁用的用户无法进行身份验证。boolean isEnabled();
}

通常来说,我们会在自己的User对象中创建authority(权限级别)、role(级别角色)两个属性,用于实现getAuthorities()。当然,如果你的项目很简单,不需要级别角色的定义,只创建authority属性也是可以的。
username和password相信不用我多说了,我们自己的User对象就有这两个属性,它们的get方法就是UserDetails的接口方法。
至于四个boolean类型的接口方法,如果项目中需要这些功能,就相应添加boolean类型的accountNonExpired、accountNonLocked、credentialsNonExpired、enabled属性。如果项目中没有使用的必要,就直接实现这四个方法全部设定为true即可。

User对象实现UserDetails

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements UserDetails {private Integer id;// 此处,用户名为email,并不是一定要取名username、password、authority、role,实现UserDetails接口方法时区分好这些字段即可。// 如果你的User对象还有其他额外的字段,对UserDetails的实现是完全没有影响的,保持它们的原样即可。private String email;private String password;// 注意,此处的Authority和Role是我定义的枚举类,这也是权限字段常用的定义方式。private Authority authority;private Role role;// 账号权限@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {// 此处注意,因为UserDetails把authority和role都放入了authoritie属性中,所以Security规定role前加上级别角色标识符“ROLE_”,以便区分authoritie列表中哪些元素是authority,哪些是role。// 当然,你也可以把级别角色标识符“ROLE_”定义在枚举类属性中,此处就可以直接传参role.toString()了。return AuthorityUtils.createAuthorityList(authority.toString(), "ROLE_" + role.toString());}// 账号名@Overridepublic String getUsername() {// 因为我们的用户名为email,所以需要额外实现getUsername()方法// 而getPassword()方法不用实现,因为@Data注解已经帮我们实现return email;}// 账号没有过期@Overridepublic boolean isAccountNonExpired() {return true;}// 账号没有锁定@Overridepublic boolean isAccountNonLocked() {return true;}// 凭证未过期@Overridepublic boolean isCredentialsNonExpired() {return true;}// 账号可用@Overridepublic boolean isEnabled() {return true;}
}

Authority枚举类

import com.alibaba.fastjson.annotation.JSONType;@JSONType(serializeEnumAsJavaBean = true)
public enum Authority implements BaseEnum<Authority, Integer> {MEMBER(1, "普通成员"),ADMIN(2, "普通管理员"),SUPER(3, "超级管理员");private Integer code;private String name;Authority(Integer code, String name){this.code = code;this.name = name;}@Overridepublic Integer getCode() {return code;}@Overridepublic String getName() {return name;}public static Authority getEnum(Integer code){for (Authority value : Authority.values()) {if(value.getCode().equals(code)){return value;}}return null;}
}

Role枚举类

import com.alibaba.fastjson.annotation.JSONType;@JSONType(serializeEnumAsJavaBean = true)
public enum Role implements BaseEnum<Role, Integer> {// 如果在枚举类中直接适应Security,直接定义为ROLE_AD、ROLE_HR、ROLE_MD、ROLE_TD即可。AD(1, "行政部成员"),HR(2, "人力资源部成员"),MD(3, "市场部成员"),TD(3, "技术部成员");private Integer code;private String name;Role(Integer code, String name){this.code = code;this.name = name;}@Overridepublic Integer getCode() {return code;}@Overridepublic String getName() {return name;}public static Role getEnum(Integer code){for (Role value : Role.values()) {if(value.getCode().equals(code)){return value;}}return null;}
}

关于枚举类的使用可查看我的上一篇文章,枚举类通用接口BaseEnum有讲到它的来源与使用。

三、创建UserDetailsService对象

同样的,我们的项目中已经有了UserServiceImpl,直接让它再实现UserDetailsService接口即可。UserDetailsService是Security默认的用户信息查询接口,里面只有一个接口方法,如下:

public interface UserDetailsService {// 参数var1代表用户名,即根据用户名查询UserDetails对象,而我们使用User对象继承了UserDetails,所以相当于查询User对象。UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}

UserServiceImpl实现UserDetailsService

@Service
public class UserServiceImpl implements UserService, UserDetailsService {@Resourceprivate UserDao userDao;// 同样的,UserServiceImpl中还有大量实现我们自定义的UserService的方法,并不影响对UserDetailsService的实现@Overridepublic User selectByEmail(String emails){return userDao.selectByEmail(email);}@Overridepublic UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {// 因为我们的用户名是email,所以就是用email去查询User对象啦return this.selectByEmail(email);}
}

关于UserService和UserDao的实现,这里就不用细说了吧!
UserServiceImpl之所以要把selectByEmail()方法单独列出来,是因为loadUserByUsername()方法返回的是UserDetails对象,这个对象只包含了authorities、username、password等属性,这是Security想要的,但不是我们想要的,我们想要的是完整的User对象,selectByEmail返回的正是User对象,下文会讲到它的使用。

四、配置DataSource

这一步就很常规了,既然都涉及到了查询数据库的用户信息,那么,对于数据源的配置当然是不可少的。

spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/test?characterEncoding=utf-8&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=123456

五、创建SecurityConfig配置类

注意,这一步信息量可就大了,代码的每一行注释都值得仔细阅读。

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter implements ProjectConstant {@Autowiredprivate UserService userService;@Autowiredprivate DataSource dataSource;// 自定义登录失败处理器,下文会给出@Autowiredprivate MyFailureHandler failureHandler;// 自定义登录验证码过滤器,下文会给出@Autowiredprivate CaptchaFilter captchaFilter;// 封装PersistentTokenRepository对象,用于辅助实现基于Cookie和数据库的remember-me记住我功能,下文会讲到。@BeanPersistentTokenRepository tokenRepository(){JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();tokenRepository.setDataSource(dataSource);return tokenRepository;}@Overridepublic void configure(WebSecurity web) throws Exception {// 设置Security忽略静态资源的访问拦截web.ignoring().antMatchers("/static/**");}// 自定义登录验证逻辑@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.authenticationProvider(new AuthenticationProvider() {@Overridepublic Authentication authenticate(Authentication authentication) throws AuthenticationException {// 获取前端登录表单传来的username值和password值,Security规定前端的请求参数一定要为”username“和”password“。String username = authentication.getName();String password = (String) authentication.getCredentials();// 使用selectByEmail()方法查询完整的User对象,而不用loadUserByUsername()方法// 这也是自定义登录验证逻辑的好处,如果采用Security默认的登录逻辑,使用的就是loadUserByUsername()方法User user = userService.selectByEmail(username);if(ObjectUtils.isEmpty(user)){throw new UsernameNotFoundException("用户名不存在!");}// 注意,用户注册时我使用了BCryptPasswordEncoder.encode()方法对密码进行了加密,于是,登录验证时也需要使用它验证密码是否正确。BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();// 注意,再使用BCryptPasswordEncoder.encode()方法对password进行加密,然后使用equals方法进行比较是错误的。// BCryptPasswordEncoder每次的加密结果都不一样,需使用matches()方法才能验证密码是否正确。boolean matches = passwordEncoder.matches(password, user.getPassword());if(!matches){throw new BadCredentialsException("密码不正确!");}// 登录成功后,将完整的User对象,password、authorities交给Security缓存。// 在业务代码中,我们想要获取当前登录User对象,直接读取Security的缓存数据即可,下文会给出具体的读取方法。return new UsernamePasswordAuthenticationToken(user, password, user.getAuthorities());}// 固定写法,标识项目使用传统的用户名与字符串密码的方式登录,而不是使用第三方扫码登录、人脸识别登录等高级登录方式。@Overridepublic boolean supports(Class<?> aClass) {return UsernamePasswordAuthenticationToken.class.equals(aClass);}});}// 自定义认证与授权逻辑@Overrideprotected void configure(HttpSecurity http) throws Exception {//加入自定义的验证码过滤器,在用户名密码过滤器之前生效http.addFilterBefore(captchaFilter, UsernamePasswordAuthenticationFilter.class);// 指定登录路径,指定登录失败逻辑,loginPage为get访问,loginProcessingUrl为post提交,两者一个指登录页面访问路径,一个指登录表单提交路径,是不一样的。// defaultSuccessUrl是登录成功后的默认跳转路径,如果用户第一次访问的是登录页面,他登录后将跳转到main页面,如果用户是访问其他页面被拦截到登录页,他登录成功后将回到之前的被拦截页。// 这一点是Security做得比较好的,这也是不用自定义登录成功逻辑(successHandler)的原因。如果自定义登录成功后的跳转逻辑,还真做不到页面拦截记忆的效果。http.formLogin().loginPage("/login").loginProcessingUrl("/login").defaultSuccessUrl("/main").failureHandler(failureHandler);// 基于数据库保存记录的记住我功能,30天免登录,共计2592000秒。Security规定前端传递的记住我参数为”remember-me“,boolean型。http.rememberMe().tokenRepository(tokenRepository()).tokenValiditySeconds(86400 * 30).userDetailsService((UserDetailsService) userService);// 指定退出登录路径,自定义登出逻辑,注意,最新的SpringSecurity版本已默认登出url为post提交方式,get提交方式不能被Security识别。http.logout().logoutUrl("/logout").logoutSuccessHandler(new LogoutSuccessHandler() {@Overridepublic void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {response.sendRedirect(request.getContextPath() + "/login");}});// 绑定访问路径与用户权限之间的关系http.authorizeRequests()// login、forget(找回密码)、register(注册)、captcha(获取验证码图片)、mail/activate(获取邮箱验证码)等请求不用登录认证.antMatchers("/login", "/forget", "/register", "/captcha", "/mail/activate","/404", "/500").permitAll()// 普通管理员能访问的功能.antMatchers("/admin", "/admin/*").hasAuthority(Authority.ADMIN.toString())// 超级管理员才能访问的功能.antMatchers("/super", "/super/*").hasAuthority(Authority.SUPER.toString())// 普通管理员和超级管理员都可以访问的功能.antMatchers("/manager", "/manager/*").hasAnyAuthority(Authority.SECRETARY.toString(), Authority.ADMIN.toString())// 普通技术员工就可以访问的功能// 如果你的Role枚举类在设计时已经携带了级别角色前缀”ROLE_“,直接传参Role.ROLE_TD.toString()即可。.antMatchers("/member/technolog").hasRole("ROLE_"+Role.TD.toString())// 只有普通管理员和超级管理员中的技术部成员才能访问的功能// 注意,Security关于这方面的功能我并没有实证过,Security是否会先检查admin/*请求需要admin权限,然后再检查admin/technolog请求需要ROLE_TD角色,这需要读者自行鉴定。.antMatchers("/admin/technolog", "/super/technolog", "/manager/technolog").hasRole("ROLE_"+Role.TD.toString()).anyRequest().authenticated();// 自定义用户没有登录或者没有权限时的处理方式http.exceptionHandling()// 用户未登录.authenticationEntryPoint(new AuthenticationEntryPoint() {@Overridepublic void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {String xRequestedWith = request.getHeader("X-Requested-With");// 异步请求if("XMLHttpRequest".equals(xRequestedWith)){response.setContentType("application/json;charset=utf-8");PrintWriter writer = response.getWriter();writer.write(JSON.toJSONString(R.error(403, e.getMessage())));}else{// 同步请求response.sendRedirect(request.getContextPath() + "/login");}}})// 用户没有相应页面的操作权限.accessDeniedHandler(new AccessDeniedHandler() {@Overridepublic void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {String xRequestedWith = request.getHeader("X-Requested-With");// 异步请求if("XMLHttpRequest".equals(xRequestedWith)){response.setContentType("application/json;charset=utf-8");PrintWriter writer = response.getWriter();writer.write(JSON.toJSONString(R.error(403, e.getMessage())));}else{// 同步请求request.setAttribute(ATTRIBUTE_MESSAGE, R.error(e.getMessage()));request.getRequestDispatcher("/404").forward(request, response);}}});// 禁用SpringSecurity默认使用X-Frame-Options防止网页被Frame,如果项目中使用到iframe层弹窗需要禁用它。http.headers().frameOptions().disable();// 不禁用SpringSecurity CSRF安全认证,开启拦截CSRF网络攻击的功能//http.csrf().disable();}
}

自定义登录失败处理器MyFailureHandler

@Component
public class MyFailureHandler extends SimpleUrlAuthenticationFailureHandler implements ProjectConstant {@Overridepublic void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {/** SpringSecurity认证失败后默认返回302状态码* 302状态码会引起前端发起 /login重定向,从而覆盖转发内容* 经测试,直接通过response.setStatus(403)是无法修改成功的* SpringSecurity依然会修改状态码为302,细节原因暂时不清* 经测试,在Controller层添加 /login post方法后,response.setStatus()能设置成功* 而且此时不通过response.setStatus()修改状态码也能正常转发* 暂且把这种解决方法称作一个善意的欺骗,避免SpringSecurity产生302状态码** 在解决此问题之前,我采用的是重定向传参的方法,但要解决中文参数乱码的问题* String msg = URLEncoder.encode(e.getMessage(), "UTF-8");* response.sendRedirect(request.getContextPath() + "/login?msg="+ msg);*/request.setAttribute(ATTRIBUTE_MESSAGE, R.error(exception.getMessage()));request.getRequestDispatcher("/login").forward(request, response);}
}

自定义验证码过滤器CaptchaFilter

@Component
public class CaptchaFilter extends OncePerRequestFilter implements ProjectConstant {@Autowiredprivate MyFailureHandler failureHandler;@Autowiredprivate RedisTemplate redisTemplate;@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {// 判断是否登录请求,登录请求才检查验证码if((request.getContextPath()+"/login").equals(request.getRequestURI()) && "POST".equals(request.getMethod())){/*下列方法是检验验证码是否正确,通过failureHandler将验证码错误信息返回给前端需要加return,不然filterChain.doFilter又放行,将导致response has been committed错误,由于全局异常类只能处理Controller层,所以需要手动捕获异常*/try {validateCaptcha(request);}catch (CaptchaException e){failureHandler.onAuthenticationFailure(request, response, e);return;}filterChain.doFilter(request, response);}filterChain.doFilter(request, response);}private void validateCaptcha(HttpServletRequest request) {// 此处自己规定前端验证码参数必须为”captcha“String captcha = request.getParameter(”captcha“);if(StringUtils.isBlank(captcha)){throw new CaptchaException("验证码不能为空!");}String captchaKey = RedisKeyUtils.getCaptchaKey(request.getRemoteAddr());Object captchaRedis = redisTemplate.opsForValue().get(captchaKey);if(ObjectUtils.isEmpty(captchaRedis)){throw new CaptchaException("验证码已失效,请刷新!");}String captchaLogin = (String) captchaRedis;if(!captcha.toUpperCase().equals(captchaLogin)){throw new CaptchaException("验证码错误!");}}
}

此处,验证码文本我是存储在Redis中的,用request.getRemoteAddr()获取客户端IP地址作为验证码的拥有者。初学者可以存储在Session中,Session对每个用户都是独立空间,不用额外指定验证码的所有者。

Controller层的配合

Security默认会生成一个简陋的登录页,只能输入用户名和密码,没有remember-me复选框,更没有图形验证码,所以自定义我们自己的登录页肯定是必须的。这就需要Controller层的SpringMVC方法配合。

// return "login"就是返回我们自定义的登录视图页面了,这个前端页面就不用我给出来了吧!
// 这个MCV方法之所以同时绑定了POST请求,是因为我在MyFailureHandler代码注释中讲到的善意的欺骗,算是Security使用中的一个坑吧,以这种巧妙的方式解决了。
@RequestMapping(value = "/login", method = {RequestMethod.POST, RequestMethod.GET})
public String login(){return "login";
}
引申

提到Controller层,其实Security也支持在Controller层以注解的方式绑定请求路径和用户权限的关系。常用的注解有@Secured和@PreAuthorize。

// 表示只有技术部成员才能访问的功能
@Secured("ROLE_TD")
@ResponseBody
@GetMapping("/member/technolog")
public String teacher(){// 业务代码略return JSON.toJSONString(R.ok());
}// 表示只有普通管理员中的技术部成员才能访问的功能
// 之前在SecurityConfig中写的hasAuthority与hasRole配合使用的例子我不知道对不对,但用注解@PreAuthorize的这个写法我确定是对的
@PreAuthorize("hasAuthority('ADMIN') && hasRole('ROLE_TD')")
@ResponseBody
@GetMapping("/admin/technolog")
public String manager(){// 业务代码略return JSON.toJSONString(R.ok());
}

前端Thymeleaf模板配合

SpringSecurity开启拦截csrf网络攻击的功能后,会默认在前端所有的form表单增加一个隐藏框,用于识别当前访问环境的安全性。如下:

<input type="hidden" name="_csrf" value="XXXXXXXXXXXXXXXXXXXXXXXX" />

那么,表单请求的确是自动携带了_csrf值,异步请求怎么办呢,异步请求不会自动携带_csrf值,默认会被Security拦截。这时候就需要我们手动携带_csrf值了,以AJAX请求为例:
1、在HTML页面指定meta标签传值_csrf.headerName与_csrf.token。

<meta name="_csrf_header" th:content="${_csrf.headerName}">
<meta name="_csrf" th:content="${_csrf.token}">

2、在JavaScript脚本中指定JAX请求携带CSRF令牌

$(document).ajaxSend(function (e, xhr, options) {var key = $("meta[name='_csrf_header']").attr("content");var value = $("meta[name='_csrf']").attr("content");xhr.setRequestHeader(key, value);
});

由此,AJAX异步请求就可以正常访问了。

引申

之前,我们遗留了一个问题,就是Thymeleaf为支持Security提供的依赖有什么作用?这里既然讲到了Thymeleaf,就简单提一下那个依赖的使用。
1、声明此依赖在前端模板中的对象名,就像声明th="http://www.thymeleaf.org"一样。

<html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">

2、使用sec对象

<div sec:authorize ="hasAuthority('ADMIN')" ><!--只有拥有ADMIN权限的用户才能看到的页面元素-->
</div >
<div sec:authorize ="hasRole('ROLE_TD')" ><!--只有拥有ROLE_TD角色的用户才能看到的页面元素-->
</div >

可以看出,Thymeleaf提供的这个依赖是为了契合Security的配置习惯,用与后端配置类相同的hasAuthority与hasRole语句来绑定前端页面显示与用户权限的关系。
然而,在实际开发中,登录用户的User对象我们肯定会传给前端的,即我们之前缓存在Security的User对象。在所有设计转发视图的SpringMVC方法中,我们都会把User对象绑定到视图中,根据这个User对象依然可以实现判断当前用户权限的目的,进而使用th对象就可以绑定前端页面显示与用户权限的关系,不用引入sec。
1、我们可以在后端创建一个工具类,获取Security缓存的User对象,供业务层代码调用,如下:

@Component
public class SecurityHolder {@SneakyThrowspublic User getUser() {Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();if(principal instanceof User){return (User)principal;}throw new PrincipalException("SpringSecurity保存的Authentication对象中主要信息Principal无法转换为User对象或者为空");}public String getUsername(){return SecurityContextHolder.getContext().getAuthentication().getName();}
}

2、SpringMVC方法绑定视图与数据

@GetMapping("/main")
public String main(Model model){User user = securityHolder.getUser();model.addAttribute("user", user);return "main";
}

3、Thymeleaf识别User对象做到前端权限隔离

<div th:if ="${user.authority.code == 2}" ><!--只有拥有ADMIN权限(code值为2)的用户才能看到的页面元素-->
</div >
<div th:if ="${user.role.code == 4}" ><!--只有拥有ROLE_TD角色(code值为4)的用户才能看到的页面元素-->
</div >

SpringSecurity单体应用相关推荐

  1. SpringSecurity系列学习(一):基于JWT的认证

    版权声明:本文由神州数码云基地团队整理撰写,若转载请注明出处 代码参考 憋嗦话,上号! 咳咳,在上号之前,再聊两句... 分布式认证 分布式认证,即我们常说的单点登录,简称SSO,指的是在多应用系统的 ...

  2. SpringSecurity OAuth2.0认证授权-part1

    在学习此之前回顾一下SpringSecurity认证授权原理. 此篇文章包含springsecurity认证授权回顾及分布式系统认证方案: 篇幅过长,拆分为: part1: 认证授权原理回顾及分布式系 ...

  3. Java 单体服务开发指南

    文章目录 一.代码组织模式 1.多仓库 2.单体仓库 二.编程规约(参考<阿里 Java 开发手册>) 1.命名风格 2.常量定义 3.代码格式 4.OOP 规约 5.日期时间 6.集合处 ...

  4. SpringSecurity最全实战讲解

    文章目录 Spring Security 专题 一.基本概念 认证 授权 会话 RBAC模型 二.一个自己实现的权限模型 BasicAuth: 三.SpringBoot Security 快速上手 1 ...

  5. SpringSecurity安全验证中文乱码问题

    使用SpringSecurity做安全验证时发现form表单中提交中文名会出现乱码问题. 原因是因为我在web.xml配置文件中将springSecurityFilterChain拦截器放在了 cha ...

  6. Spring源码分析【8】-分布式环境SpringSecurity保持用户会话

    1.SpringSecurity的权限控制流程是这样的: 用户登录,基础信息UserInfo存在SpringSecurity的ThreadLocal里. 下面是contextHolder对象: fin ...

  7. SpringSecurity学习:1(第一个SpringSecurity项目)

    此博客是记录自己学习过程的记录 第一个SpringSecurity项目 导入依赖 详细的步骤我就不多说了,使用IDEA创建过SpringBoot项目的人一般都能看懂. 这一步我们可以在使用IDEA创建 ...

  8. Java项目:在线淘房系统(租房、购房)(java+SpringBoot+Redis+MySQL+Vue+SpringSecurity+JWT+ElasticSearch+WebSocket)

    源码获取:博客首页 "资源" 里下载! 该系统有三个角色,分别是:普通用户.房屋中介.管理员.普通用户的功能:浏览房屋信息.预约看房.和中介聊天.申请成为中介等等.房屋中介的功能: ...

  9. SpringSecurity使用 配置文件 和wen.xml 文件配置

    目录 1.web.xml 文件配置 2.spring-security  普通 为使用自己创建的认证类 1.web.xml 文件配置 !-- 配置SpringSecurity的拦截器 -->&l ...

最新文章

  1. python3调用腾讯AI开放平台
  2. 如何备考上海市高等学校计算机一级,如何备考全国计算机一级等级考试
  3. 任意门怎么用团发_衣柜门选用什么材料好?小编在这里告诉你
  4. python中的property_python中的property属性
  5. Large Memory Footprints on AIX
  6. 量子密钥和量子计算机是什么关系,关于量子通信,这些问题你困惑过吗?
  7. 【算法】剑指 Offer 31. 栈的压入、弹出序列 【重刷】
  8. asynchronous vs non-blocking
  9. AST语法结构树初学者完整教程
  10. windows 10
  11. python中read() readline()以及readlines()用法
  12. win7下MongoDB集群告别裸奔
  13. 分布式配置管理平台 - Disconf介绍
  14. 拖拽图片到另一个div里
  15. 新手JDK下载与安装教程
  16. [Zer0pts2020]ROR1
  17. matlab基础与实例教程,MATLAB R2018基础与实例教程
  18. 一款用C++语言实现的3D游戏引擎(附源码),适用于想学3D游戏开发
  19. Apicloud+Vue开发App专题
  20. vmware mac os 10.11.6 安装xcode 8

热门文章

  1. 也说TCP/IP之OSI七层模型
  2. java实现大段中文转拼音首字母、拼音全拼
  3. 爬虫训练场项目前端基础,Bootstrap5排版、表格、图像
  4. 深入学习 jQuery 选择器系列第三篇——过滤选择器之索引选择器 - 小火柴的蓝色理想 - 博客园...
  5. 用友ERP-U8为奥运供应商信息化加油---万象汽车ERp
  6. 计算机思维在数学中的应用,浅谈数学思维方式在计算机教学中的应用
  7. 分布式session详解
  8. 利用Java实现福彩双色球项目(支持单式/复式购买)
  9. java项目横向越权_横向越权、纵向越权问题解决
  10. 【学习笔记】大数据技术之Hive(下)