前言

首先,集成spring-security的目的
1,实现登录控制;
2,防止同一账号的同时多处登录。
3,实现台接口的访问权限控制。

实现方式不止一种,选择spring-security是因为它够简洁。

实现

阐述两种实现方式--不用框架、采用spring-security

不用框架

问题1

不用框架的话,实现前言中的问题1(下文简称:问题1)可以在每次请求时,先获取下session,然后判断下该session是否已经登录。
如何判断是否登录?可以往session中插入内容嘛,其实就是将该session与具体的某个帐号关联起来。
具体实现不赘述了,因为这种方式实在太普遍了,百度下满地都是,它不是我要记叙的重点。

PS:每次请求都去判断下是不是很繁琐,这时候你该考虑拦截器,前置处理所有请求
问题2

解决问题2也简单,统一维护所有session,新加入的session和老的比对下。如果映射的帐号是同一个就执行控制策略,比如:踢掉旧的,保留新的。

问题3

一样,原理是控制每次请求时,该session对应帐号的权限,拦截器可以有效统一处理。

采用spring-security

每次遇到普遍性的问题,就该去想想这类是否有统一的解决方法。

针对上述3个问题,找到了spring-security,而且它不仅仅局限于此,只是我暂时只需要用到它这3个功能。spring全家桶越用越舒服,我是真的佩服这些做开源免费软件的。

spring-security解决上述3个问题,就是一份配置
import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.util.DigestUtils;

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
	@Autowired
    UserService userService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService).passwordEncoder(new PasswordEncoder() {
            @Override
            public String encode(CharSequence charSequence) {
                return charSequence.toString();
            }

            /**
             * @param charSequence 明文
             * @param s 密文
             * @return
             */
            @Override
            public boolean matches(CharSequence charSequence, String s) {
                return s.equals(charSequence.toString());
            }
        });
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
//      			.antMatchers("/test/security/**").hasRole("超级管理员")
                .anyRequest().authenticated()//其他的路径都是登录后即可访问
                .and().formLogin().loginPage("/test/security/login").successHandler(new AuthenticationSuccessHandler() {
            @Override
            public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, 
            		Authentication authentication) throws IOException, ServletException {
                httpServletResponse.setContentType("application/json;charset=utf-8");
                PrintWriter out = httpServletResponse.getWriter();
                out.write("{\"status\":\"ok\",\"msg\":\"登录成功\"}");
                out.flush();
                out.close();
            }
        }).failureHandler(new AuthenticationFailureHandler() {
        	@Override
        	public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, 
        			AuthenticationException e) throws IOException, ServletException {
                httpServletResponse.setContentType("application/json;charset=utf-8");
                PrintWriter out = httpServletResponse.getWriter();
                out.write("{\"status\":\"error\",\"msg\":\"登录失败\"}");
                out.flush();
                out.close();
            }
        }).loginProcessingUrl("/test/security/loginP")//登录地址
        .usernameParameter("username").passwordParameter("password").permitAll()
        .and().logout().permitAll().and().csrf().disable()
        .cors();//添加cors支持跨域
        
        http.sessionManagement().maximumSessions(1).expiredUrl("/test/security/login");
        //防止多处登录
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/error", "/swagger-ui.html", 
				"/back/admin/getCountInfo", "/swagger-resources/**", "/v2/api-docs",
				"/api/**", "/pub/**");//pub:用于测试,swagger测试用?
    }
    
}

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Service
public class UserService implements UserDetailsService {

	@Autowired
	private UserRepo userRepo;
	
	@Override
	public UserDetails loadUserByUsername(String name) throws UsernameNotFoundException {
		return userRepo.findFisrtByName(name);
	}

}
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

@Entity
public class UserEnt implements UserDetails {

	@Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    private Long id;
	private String name;
	private String passwd;
	private String role;
	
	public UserEnt() {}
	
	public UserEnt(String name, String passwd, String role) {
		this.name = name;
		this.passwd = passwd;
		this.role = role;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public String getPasswd() {
		return passwd;
	}

	public void setPasswd(String passwd) {
		this.passwd = passwd;
	}

	public String getRole() {
		return role;
	}

	public void setRole(String role) {
		this.role = role;
	}

	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		List<GrantedAuthority> authorities = new ArrayList<>();
		authorities.add(new SimpleGrantedAuthority(role));
		return authorities;
	}

	@Override
	public String getPassword() {
		return passwd;
	}

	@Override
	public String getUsername() {
		return name;
	}

	@Override
	public boolean isAccountNonExpired() {
		return true;
	}

	@Override
	public boolean isAccountNonLocked() {
		return true;
	}

	@Override
	public boolean isCredentialsNonExpired() {
		return true;
	}

	@Override
	public boolean isEnabled() {
		return true;
	}
	
	@Override
	public String toString() {
		return this.name;
	}
	
	@Override
	public int hashCode() {
		return name.hashCode();
	}
	
	@Override
	public boolean equals(Object obj) {
		return this.toString().equals(obj.toString());
	}
	
//	@Override
//	public boolean equals(Object obj) {
//		UserEnt target = (UserEnt) obj;
//		return name.equals(target.getName());
//	}

}

代码不多贴了,能看清关键逻辑即可。其实很多博文让人观感难受的一大原因就是–上来一堆代码,全文无总结。当然,我也常这么干,因为博客的首要作用是自我总结,先是自己的笔记本,之后才是赠人的玫瑰。但是这篇我还是想记叙的明白一些。

首先,我spring-security解决这3个问题的原理其实也就是对session的维护以及对各接口的权限控制。所以,配置逻辑其实就是围绕着这个来的。

归纳下配置逻辑:

1,spring-security需要建立session和用户的关系。所以,userService实现了UserDetailsService这个接口,这个接口是用来获取用户信息的。
2,登录是传入的密码通常是加密的,你可以在第一个configure中做相应处理。我 只是做测试所以未处理,具体可以参考下文的几篇参考链接,这类问题百度不难解决。
3,配置登录地址,登录页面,登录成功及失败后反馈。
4,总有一些接口不想被拦截的,那么就需要在最后一个configure中剔除掉。

解释

这部分及接下去的要点其实才是最重要的,是对关键方法的说明。那些直接copy然后看着方法名和注释就能明白的部分就不赘述了。
1,loginPage中的参数是登录页。所谓登录页,就是未登录时访问在拦截范围内的页面会触发302跳转到此处。它不非得是一个页面,可以是返回一串json内容。所以,很灵活是不是。
2,loginProcessingUrl是spring-security的登录地址。你想啊,你得让spring-security来维护session,要么是你把session甩给它,要么是直接经过它登录,总得让它能把session和用户关联起来。最简单的就是后者–直接经过它登录,然后它会根据登录结果来控制各接口访问权限。
3,successHandler&failureHandler就是登录成功和失败后的返回。
4,防止多处登录其实就是增加一句代码配置。此处的配置逻辑是–新的登录覆盖旧的,可以按需配置为不允许新的再次登录,此处不展开了。
5,todo:接口权限控制,暂时没测试。

小结,其实spring-security就是把本来该我们自己做的事,替我们做了,而且做得更更好。
要点

这里,还有几个要点要说下:
1,loginProcessingUrl中定义的spring-security登录地址必须用post方式请求,get是无效的会被一并拦截。
2,要防止多处登录,首先就是用户的比对,所以,实现UserDetails的那个类(UserEnt)的equal方法写的时候规范点。
3,要开启跨域的话,原先springboot的跨域配置要保留外,还得调用cors()方法。

参考

主要参考随便看看

获取当前登录用户

防止一个账户多处登录无法实现防止多处登录估计你得看这个

实现cros(这类博客的style是我最喜欢的–简明扼要)

PS:费了一天功夫呢,挂个原创不过分吧。。。

tips

1,未被security拦截的请求中无法获取security相关的注解