考虑到Vue本身是一个渐进式的前端框架,在被案例中并没有使用Vue的页面组件化和路由,而是采用html页面+iframe(代替路由)。
同时,为了省去手写可复用Vue组件的工作量,所以使用ElementUI作为UI组件。
最后,ajax采用的是axios组件。
注意:即便后端采用的不是springsecurity进行登录,也有很多知识是相通的。
阳了三天,基本不烧了,只是听觉丧失50%,味觉丧失70%,献给CJ104那些爱卷的猴崽子们吧:)
先不解释概念直接上代码
从数据库中查询用户和密码信息,用于替换SpringSecurity自动产生密码
@Component
public class OAUserDetailService implements UserDetailsService {
// 鉴于篇幅的原因,就不贴UserService的代码了,具体实现就是根据用户名查用户记录并返回
@Resource
private UserService userService;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
User user = userService.getById(s);
if (user == null) throw new UsernameNotFoundException(String.format("不存在%s用户", s));
return new OAUserDetail(user);
}
public class OAUserDetail implements UserDetails{
private User user;
public OAUserDetail(User user){
this.user = user;
}
@Override
public Collection extends GrantedAuthority> getAuthorities() {
return new ArrayList<>();
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getId();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
}
SpringSecurity配置
@Configuration
@Slf4j
public class OASecurityConfiguration extends WebSecurityConfigurerAdapter {
// 临时方案,实战中不可取
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.successForwardUrl("/login/success") // 登录成功跳转到controller处理
.failureForwardUrl("/login/failure");
http.cors();
http.csrf().disable();
http.authorizeRequests().anyRequest().authenticated(); // 没有这一句,将会直接绕过登录可以访问到controller
http.exceptionHandling().authenticationEntryPoint(new OAAuthenticationEntryPoint());
}
@Bean
CorsConfigurationSource corsConfigurationSource(){
CorsConfiguration corsConfiguration = new CorsConfiguration();
// 允许跨域访问的站点
corsConfiguration.setAllowedOrigins(Arrays.asList("http://127.0.0.1:5500"));
//允许跨域访问的methods
corsConfiguration.setAllowedMethods(Arrays.asList("GET","POST"));
// 允许携带凭证
corsConfiguration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
//对所有URL生效
source.registerCorsConfiguration("/**",corsConfiguration);
return source;
}
}
登录处理Controller
省略了演示过很多遍的R对象的代码,就是一个类包含了code、message、data三个属性的前后端分离响应类
@RestController
@RequestMapping("/login")
public class LoginController {
@RequestMapping(value = "/success", method = {RequestMethod.GET, RequestMethod.POST})
public R loginSuccess(){
return R.error(401, "登录成功");
}
@RequestMapping(value = "/failure", method = {RequestMethod.GET, RequestMethod.POST})
public R loginFailure(){
return R.ok("登录失败");
}
}
未登录异常处理类
public class OAAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
response.setContentType("application/json;charset=UTF-8");
PrintWriter out = response.getWriter();
R r = R.error(401, "未登录");
out.write(new ObjectMapper().writeValueAsString(r));
out.flush();
out.close();
}
}
登录页
登录奇豆OA
登录
首页
奇豆OA
{{item.name}}
{{sitem.name}}
前后端分离的的具体表现有两点
1、前端采用html技术来开发页面
2、前端页面与服务端Servlet分开部署在不同的web容器中
好处很明显:前端工程师不需要学习Servlet技术、前后端并行开发、减轻服务端Servlet容器的访问压力...
但带来的问题就包括:
跨域访问、登录session、Json响应,接下来将基于之前的代码针对这几个问题进行探讨
跨域是指在不同的域之间进行访问存在安全隐患,所以对请求进行了限制。跨域分为以下三种情况
http://127.0.0.1:8080 --> https://127.0.0.1:8080 协议跨域
http://127.0.0.1:8080 --> http://127.0.0.2:8080 IP跨域
http://127.0.0.1:8080 --> http://127.0.0.1:8081 端口跨域
注意,如下两种情况不涉及跨域
1 如果是前后端不分离,即将页面和服务端代码运行在同一个Servlet容器中自然不涉及跨域
2 直接在浏览器中输入服务端Controller的URL,相当于local --> server 也谈不上跨域
解决跨域的办法有好几种,基于之前的案例代码,我们采用服务端解决跨域
非SpringSecurity的场景
import org.springframework.http.HttpHeaders;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration corsConfiguration = new CorsConfiguration();
// 只允许127.0.0.1:5500跨域访问
corsConfiguration.addAllowedOrigin("http://127.0.0.1:5500");
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
corsConfiguration.addExposedHeader(HttpHeaders.COOKIE);
corsConfiguration.setAllowCredentials(true);
source.registerCorsConfiguration("/**", corsConfiguration);
return new CorsFilter(source);
}
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://127.0.0.1:5500")
.allowedMethods("GET","POST")
.allowCredentials(true)
.allowedHeaders("*");
}
}
注意:需要引入的依赖包org.springframework.web.cors.xxx,所以特意贴了import代码
SpringSecurity的场景
在使用了springSecurity的情况下你会发现之前的@CrossOrigin注解也解决不了跨域问题了,原因是springsecurity默认情况下并不允许跨域访问,通过如下设置可以解决该问题
@Configuration
@Slf4j
public class OASecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// 开启允许跨域访问
http.cors();
}
@Bean
CorsConfigurationSource corsConfigurationSource(){
CorsConfiguration corsConfiguration = new CorsConfiguration();
// 允许跨域访问的站点
corsConfiguration.setAllowedOrigins(Arrays.asList("http://127.0.0.1:5500"));
//允许跨域访问的methods
corsConfiguration.setAllowedMethods(Arrays.asList("GET","POST"));
// 允许携带凭证
corsConfiguration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
//对所有URL生效
source.registerCorsConfiguration("/**",corsConfiguration);
return source;
}
}
至此,跨域问题基本可以解决了
那么接下来就会发现一个问题,登录成功后index页面仍然无法发起请求(根据ParentID获取菜单列表)
该问题就是没有session ID导致的
session是一种用来解决http协议无状态的重要技术。具体是如何解决无状态的呢?
首先,当浏览器访问服务端,且使用的session时,servlet容器(Tomcat)会为其创建一个Session对象,并为其生存一个session id(JSESSIONID),并通过写cookie的方式将sesssion id写入到浏览器的cookie中,如下图
然后,浏览器将自动保存session ID
如下图
最后,当浏览器再次发起其他请求时,将会自动的携带该session ID,如下图(总算把线划直了)
结论:session + cookie解决http无状态的问题
嗯.... 这和登不登录有和关系???
session的创建并不是用来解决登录问题的,但是,我们可以通过这一技术来简单的实现登录,这取决于登录时创建session,并将用户信息存入session,来点伪代码吧
// 由于是伪代码所以就没有写注解了,方法的返回值就随意些一下,不要照抄
public class LoginController{
public String login(HttpServletRequest request, String username, String password){
// 执行这句将会创建Session
Session session = request.getSession()
// 根据用户名获取用户信息
User use = getUser(username)
// 验证密码
boolean result = verifyLogin(use, password)
// 保存用户信息到session,注意不要保存密码等敏感信息到Session
session.setAttribute("user", username)
}
}
这样一来,我们就可以在拦截器中取检查对于session id对于的session中是否有user属性
经过上一小节的说明,相信大家对session id与登录的关系了。但是,在跨域的情况下浏览器并不能正常的保存session id。
注意:session id的话题与springsecurity无关,无论采用什么方式实现登录都一样。除非采用Token记录登录的方式
浏览器在跨域的情景下不能正常保存session id,其根本原因是ajax的异步请求,需要通过在异步回调中为浏览器保存session id。
在全景代码中通过Axios发起ajax请求有如下一段代码
new Vue({
el: '#app',
data: {...},
methods: {
login(){
axios({
method: 'post',
url: 'http://127.0.0.1:8080/login',
// 就是他啦~~~~~~~~~~~~
withCredentials: true,
data: Qs.stringify({
username: this.form.username,
password: this.form.password
})
}).then((response) => {...})
}
}
})
withCredentials: true,的选项则意味着浏览器保存认证信息(即保存session id),同时,也会在发起请求时自动带上seesion id。所以,所有的请求的需要加上该选项。
至此,session id的问题可以解决
但是,前后端分离的一大特性就是,前后端之间通过JSON来传递响应结果,那么,在spring security接管登录处理、登录失败、登录成功等处理之后如何替换掉用默认的页面跳转,而改为返回Json响应呢。
对于全局代码中Security配置类关键代码为:
@Configuration
@Slf4j
public class OASecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// 就他啦~~~~
http.exceptionHandling().authenticationEntryPoint(new OAAuthenticationEntryPoint());
}
}
http.exceptionHandling().authenticationEntryPoint(new OAAuthenticationEntryPoint());
意味着,当未登录时会触发异常,并触发EntryPoint(即,跳转登录页),只需要自己定义该EntryPoint便可以接管默认行为,改为返回Json响应了。
至于代码中的OAAuthenticationEntryPoint,在全景代码部分已经有贴过,就不重复了
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.successForwardUrl("/login/success")
.failureForwardUrl("/login/failure");
}
successForwardUrl与failureForwardUrl
可以指定Controller来处理登录成功与失败。至于该Controller也在全景代码部分已经有贴过,就不重复了
至此,前后端分离的登录的问题已经解决。
注意:前端部分axios代码,需自行优化