前言
首先,集成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相关的注解