近期在搞微服务,看到OAuth2认证登录的时候,突然就搞不明白了,自己对于spring security 压根就没搞清楚,于是花了几天时间专门看了看ss,把这几天自己学到的记录一下。
老规矩,先说明概念。
1.什么是spring security
ss为我们的应用程序提供了认证、和授权两大功能,这对于我们应用程序的安全至关重要,它决定着什么用户或角色可以访问我们的程序的任何一角。ss整个架构的核心也是围绕这两个功能来实现的。
2.框架原理
对于web程序,要想保护好我们的资源那么过滤器(filter)将是不二选择,要想对方法调用进行保护,最好的方法莫过于AOP,而ss也是通过各种过滤器链,层层结构过滤筛选从而达到保护我们应用程序的目的。
(本文属于基础,对于各种过滤器不在深究,想研究的道友可以查看其它资料,本文更多的简单说一下ss框架的大体流程,偏向应用)
如果之前没有接触过ss框架的,可以先看一下 spring security框架介绍。
先引入依赖
<!--引入security-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-security</artifactId>
</dependency>
<!--模板引擎-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
我开放了三个端点分别是/login,/user,/admin
@Controller
public class hello {
@RequestMapping("/login")
public String hello(){
return "/login";
}
@RequestMapping("/user")
public String hello1(){
return "/user";
}
@RequestMapping("/admin")
public String hello2(){
return "/admin";
}
}
现在我们启动我们的应用程序
置于控制台打印了一串数字
我们访问我们的端点
ss框架默认已经起作用了,这个界面是ss框架的默认界面,ss框架默认有一个账号user,密码就是启动程序的时候控制台打印的数字。下来我们输入账号密码登录
看到我们成功访问。
自定义授权
刚才使我们使用ss默认的账号登录的,如果我们想要自定义账号并且授权登录怎么做呢?如果我们想通过自定义过滤器对不同的url请求做出不同的认证授权操作怎么办呢?
我们的项目之所以有授权功能,是ss框架使用一些默认的配置来帮我们实现的。而如果我们要实现一些自定义的配置,就需要我们继承一些类或实现一些接口来实现我们自己的逻辑,然后让ss框架采用我们的配置即可。
而我们要继承并重写自定义我们自己的认证规则的类就是WebSecurityConfigurerAdapter这个适配器。我们重写Configure方法,参数分别是HttpSecurity,AuthenticationManagerBuilder。
@Configuration
public class text extends WebSecurityConfigurerAdapter {
/**
* 重写该方法,设定用户访问权限
* 这里的http对象最终要做的事就是把对应的url-role进行映射并应用到过滤链中
* */
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/user").hasRole("USER") //用户权限
.antMatchers("/admin").hasRole("ADMIN") //管理员权限
.antMatchers("/login").permitAll() //所有用户都可以访问
.and()
.formLogin();
}
/**
* 重写该方法,添加自定义用户
* 自定义我们的用户和角色
* */
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("pk1").password("123").roles("USER")
.and()
.withUser("pk2").password("123").roles("ADMIN");
}
}
启动我们的项目,访问地址:localhost:8080
因为我们再适配器中自定义的url没有包含index过滤映射,所以正常访问,接下来我们访问一下/user端点。
自动跳转到ss框架的login页面,因为我们在之前自定义中访问/user端点需要认证并授权USER角色,我们先用框架默认的账号密码登录看是什么结果
Bad credentials 错误的凭证,因为默认的角色是没有USER这个角色的所以无法访问,那么我们下来用我们自定义的角色pk1,它拥有的角色是USER.
看到自动跳转error页面了,说明我们登录认证过程中出错了,我们来看一下控制台看是不是报错了。
这个问题是因为spring security 5.0以后添加了许多密码加密的方式,加密格式是{id}…,id是加密方式,ss拿到密码后会先检查{id},如果没有加密那么就会报null的错。这里我们简单处理一下,不用密码加密(不推荐)。
//不进行密码加密
@Bean
public static PasswordEncoder passwordEncoder(){
return NoOpPasswordEncoder.getInstance();
}
重启项目继续用pk1访问/user端点。
看到我们正常访问到user页面,那么我们接下来访问/admin端点,该端点需要admin角色,那么肯定还是去访问error页面了。
通过查询数据库的方式校验用户
那么我们正常的项目用户和角色肯定不是我们这样写死在程序里的,肯定是从其他地方拿过来然后我们自己实现校验逻辑来实现的。这些ss都给我们准备好了,实现这一点我们要先了解一些东西。
一个过滤器:AbstractAuthenticationProcessingFilter 这个过滤器用于拦截认证请求,查看官方文档我们可以知道它是基于浏览器和 HTTP 认证请求的处理器,可以理解为它就是 Spring Security 认证流程的入口。
简单说一下AbstractAuthenticationProcessingFilter 工作机制
一、AbstractAuthenticationProcessingFilter本身是一个抽象类,框架提供了几个实现类
二、而这几个实现类会实现根据抽象基类中的抽象方法
public abstract Authentication attemptAuthentication(HttpServletRequest var1, HttpServletResponse var2) throws AuthenticationException, IOException, ServletException;
根据http请求组装一个简单的Authentication对象.
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
} else {
String username = this.obtainUsername(request);
String password = this.obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
this.setDetails(request, authRequest);
//注意这里 将构造的Authentication转交给了AuthenticationManager去处理
return this.getAuthenticationManager().authenticate(authRequest);
}
}
这里要注意一点,在生成Authentication后,委派给了AuthenticationManager去处理,而AuthenticationManager并不是自身去处理,因为AuthenticationManager自身也是一个接口,框架也提供了好多它的实现类。Spring Security 提供了一个默认实现 ProviderManager,ProviderManager自身维护一个AuthenticationProvider List 列表,通过遍历List 中的AuthenticationProvider 去处理认证请求。
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
Authentication result = null;
boolean debug = logger.isDebugEnabled();
Iterator var6 = this.getProviders().iterator();
//在这里循环遍历处理请求
while(var6.hasNext()) {
AuthenticationProvider provider = (AuthenticationProvider)var6.next();
if (provider.supports(toTest)) {
if (debug) {
logger.debug("Authentication attempt using " + provider.getClass().getName());
}
try {
result = provider.authenticate(authentication);
if (result != null) {
this.copyDetails(authentication, result);
break;
}
} catch (AccountStatusException var11) {
this.prepareException(var11, authentication);
throw var11;
} catch (InternalAuthenticationServiceException var12) {
this.prepareException(var12, authentication);
throw var12;
} catch (AuthenticationException var13) {
lastException = var13;
}
}
}
if (result == null && this.parent != null) {
try {
result = this.parent.authenticate(authentication);
} catch (ProviderNotFoundException var9) {
;
} catch (AuthenticationException var10) {
lastException = var10;
}
}
if (result != null) {
if (this.eraseCredentialsAfterAuthentication && result instanceof CredentialsContainer) {
((CredentialsContainer)result).eraseCredentials();
}
this.eventPublisher.publishAuthenticationSuccess(result);
return result;
} else {
if (lastException == null) {
lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound", new Object[]{toTest.getName()}, "No AuthenticationProvider found for {0}"));
}
this.prepareException((AuthenticationException)lastException, authentication);
throw lastException;
}
}
三、因为AuthenticationProvider也是一个接口,框架也实现了很多它的实现类。我们现在不管它用的哪一个实现类,列表中的AuthenticationProvider会对Authentication请求对象进行处理,如果认证通过,会返回一个填充的Authentication对象,如果认证不通过会返回异常。认证通过后AuthenticationProvider返回给AuthenticationManager一个填充后的Authentication,AuthenticationManager继续向上返回给AbstractAuthenticationProcessingFilter 对象填充后的Authentication对象,AbstractAuthenticationProcessingFilter 把返回的Authentication对象放到SecurityContextHolder对象中,SecurityContextHolder是 Spring Security 最基础的对象,用于存储应用程序当前安全上下文的详细信息,这些信息后续会被用于授权。这就是整个的认证过程。
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Authentication success. Updating SecurityContextHolder to contain: " + authResult);
}
//看这里
SecurityContextHolder.getContext().setAuthentication(authResult);
this.rememberMeServices.loginSuccess(request, response, authResult);
if (this.eventPublisher != null) {
this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
}
this.successHandler.onAuthenticationSuccess(request, response, authResult);
}
三个接口 UserDetails,UserDetailsService,Authentication
public interface UserDetailsService {
UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
public interface Authentication extends Principal, Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
Object getCredentials();
Object getDetails();
Object getPrincipal();
boolean isAuthenticated();
void setAuthenticated(boolean var1) throws IllegalArgumentException;
}
看源码我们能看到UserDetailsService通过username来获取UserDetails,而UserDetails包含了用户的信息和权限。
这里说明一点,Authentication实现类中的Principal对象其实就是我们的UserDetails。也可以这么说UserDetailsService其实就是Authentication的一个属性。总体来看,AuthenticationProvider 在认证请求期间通过调用UserDetailsService的loadUserByUsername方法,返回一个UserDetails对象,并且最终将该对象封装进Authentication对象中,也就是作为Authentication对象的Principal变量。我们来看一个AuthenticationProvider 实现类中的方法。
public interface AuthenticationProvider {
Authentication authenticate(Authentication var1) throws AuthenticationException;
boolean supports(Class<?> var1);
}
这个是AuthenticationProvider 接口,只有两个方法,第一个方法就是接受一个Authentication对象,这个Authentication包含了我们填写认证的一些信息,而我们可以从Authentication对象中取到我们需要的信息,并借助UserDetailsService的loadUserByUsername方法获取到我们真实的账号信息,并进行匹配校验,如果校验通过,我们会把信息封装成一个完整的Authentication并返回。
具体流程说完了,我们来实现自定义认证过程,通过查数据库,查询登录账号密码正确性以及角色来授权是否能访问我们的端点。
先实现我们自己的UserDetails,首先建立一个User Bean
public class User {
//账号的几种状态
public static String STATE_ACCOUNTEXPIRED = "ACCOUNTEXPIRED"; //是否过期
public static String STATE_LOCK = "LOCK"; //是否被锁定
public static String STATE_TOKENEXPIRED = "TOKENEXPIRED"; //TOKEN是否过期
public static String STATE_NORMAL = "NORMAL"; //是否可用
private String userid;
public String getUserid() {
return userid;
}
public void setUserid(String userid) {
this.userid = userid;
}
private String username;
private String password;
private String state;
private String name;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
然后实现一个UserDetails
/**
*
* 定义自己的UserDetails
*
*
*/
public class MyUserDetails implements UserDetails {
private User user;
private Collection<? extends GrantedAuthority> authorities;
public MyUserDetails(User user, Collection<? extends GrantedAuthority> authorities) {
super();
this.user = user;
this.authorities = authorities;
}
//序列化
private static final long serialVersionUID = 1L;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return this.user.getPassword();
}
@Override
public String getUsername() {
return this.user.getUsername();
}
//是否过期
@Override
public boolean isAccountNonExpired() {
return this.user.getState().equals(User.STATE_ACCOUNTEXPIRED);
}
//是否锁定
@Override
public boolean isAccountNonLocked() {
return this.user.getState().equals(User.STATE_LOCK);
}
//凭证是否过期
@Override
public boolean isCredentialsNonExpired() {
return this.user.getState().equals(User.STATE_TOKENEXPIRED);
}
//是否禁用
@Override
public boolean isEnabled() {
return this.user.getState().equals(User.STATE_NORMAL);
}
}
接下来实现我们的UserDetailsService
@Service("userDetailsService")
public class AuthUserDetailService implements UserDetailsService {
@Autowired
private UserInfoDao userInfoDao;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserDetails userDetails = null;
try{
User user = userInfoDao.getUserinfo(username);
if(user!=null){
String rolename = userInfoDao.getRoleByUserid(user.getUserid());
Collection<GrantedAuthority> authorities = new ArrayList<>();
SimpleGrantedAuthority grant = new SimpleGrantedAuthority(rolename);
authorities.add(grant);
//封装自定义的UserDetails
userDetails = new MyUserDetails(user, authorities);
}else{
throw new UsernameNotFoundException("该用户不存在!");
}
} catch (Exception e){
e.printStackTrace();
}
return userDetails;
}
}
@Component
public class UserInfoDao {
@Autowired
public JdbcTemplate jdbcTemplate;
public User getUserinfo(String username){
String sql = "select * from myuser where username = '"+username+"'";
System.out.println(sql);
List<String> list = (List) jdbcTemplate.query(sql, new Object[] {},
new ResultSetExtractor() {
public Object extractData(ResultSet rs)
throws SQLException, DataAccessException {
List<String> userinfo = new ArrayList<String>();
while (rs.next()) {
userinfo.add(rs.getString(1));
userinfo.add(rs.getString(2));
userinfo.add(rs.getString(3));
userinfo.add(rs.getString(4));
userinfo.add(rs.getString(5));
}
return userinfo;
}
});
System.out.println(list.size());
User user = new User();
if(list.size()>0){
user.setUserid(list.get(0));
user.setUsername(list.get(1));
user.setPassword(list.get(2));
user.setState(list.get(3));
user.setName(list.get(4));
}
return user;
}
public String getRoleByUserid(String userid){
String roleid = "";
String rolename = "";
String sql = "select roleid from UserRole where userid = '"+userid+"'";
roleid = (String) jdbcTemplate.query(sql, new Object[] {},
new ResultSetExtractor() {
public Object extractData(ResultSet rs)
throws SQLException, DataAccessException {
String roleid = "";
while (rs.next()) {
roleid = rs.getString(1);
}
return roleid;
}
});
String sql2 = "select name from Role where id = ? ";
if(roleid!=null){
rolename = (String) jdbcTemplate.query(sql2, new Object[] {roleid},
new ResultSetExtractor() {
public Object extractData(ResultSet rs)
throws SQLException, DataAccessException {
String roleid = "";
while (rs.next()) {
roleid = rs.getString(1);
}
return roleid;
}
});
}
System.out.println("ROLENAME="+rolename);
return rolename;
}
}
附上我们的表结构
CREATE TABLE `myuser` (
`id` int(11) NOT NULL,
`username` varchar(255) DEFAULT NULL,
`password` varchar(255) DEFAULT NULL,
`state` varchar(255) DEFAULT NULL,
`name` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `role` (
`id` int(11) NOT NULL,
`name` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `userrole` (
`id` int(11) NOT NULL,
`userid` int(11) DEFAULT NULL,
`roleid` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
我们模拟一条数据
准备的差不多了,我们就要编写一个我们自定义的AuthenticationProvider然后配置到ss框架中就可以了。
/**
*
* 自定义认证服务
*
*/
@Service("securityProvider")
public class SecurityProvider implements AuthenticationProvider {
private AuthUserDetailService userDetailsService;
public SecurityProvider(UserDetailsService userDetailsService) {
this.userDetailsService = (AuthUserDetailService) userDetailsService;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
UsernamePasswordAuthenticationToken token
= (UsernamePasswordAuthenticationToken) authentication;
String username = token.getName();
System.out.println(username);
UserDetails userDetails = null;
if(username !=null) {
userDetails = userDetailsService.loadUserByUsername(username);
}
System.out.println("$$$"+userDetails);
// if(userDetails == null) {
// throw new UsernameNotFoundException("用户名/密码无效");
// } else if (!userDetails.isEnabled()){
// System.out.println("jinyong用户已被禁用");
// throw new DisabledException("用户已被禁用");
// }else if (!userDetails.isAccountNonExpired()) {
// System.out.println("guoqi账号已过期");
// throw new LockedException("账号已过期");
// }else if (!userDetails.isAccountNonLocked()) {
// System.out.println("suoding账号已被锁定");
// throw new LockedException("账号已被锁定");
// }else if (!userDetails.isCredentialsNonExpired()) {
// System.out.println("pingzheng凭证已过期");
// throw new LockedException("凭证已过期");
// }
System.out.println(token.getCredentials());
String password = userDetails.getPassword();
System.out.println(password);
System.out.println(userDetails.getAuthorities());
//与authentication里面的credentials相比较
if(!password.equals(token.getCredentials())) {
throw new BadCredentialsException("Invalid username/password");
}
//授权 一个userdetails表示一个principal
return new UsernamePasswordAuthenticationToken(userDetails, password,userDetails.getAuthorities());
}
@Override
public boolean supports(Class<?> authentication) {
//返回true后才会执行上面的authenticate方法,这步能确保authentication能正确转换类型
return UsernamePasswordAuthenticationToken.class.equals(authentication);
}
}
最后修改我们的配置类,账户信息不再是我们写死,而是通过我们自定义的AuthenticationProvider来操作。
/**
* 继承WebSecurityConfigurerAdapter
* 添加自定义授权
*/
@Configuration
@EnableWebSecurity
public class WebSecurityConfigure extends WebSecurityConfigurerAdapter {
@Autowired
private SecurityProvider securityProvider;
//认证管理器
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//自定义AuthenticationProvider
auth.authenticationProvider(securityProvider);
}
//自定义用户权限
//对http的操作就是在添加url-filter的关系映射
@Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/user").hasRole("USER")
.antMatchers("/admin").hasRole("ADMIN")
.antMatchers("/login").permitAll()
.and()
.formLogin()
.loginPage("/login") //跳转登录页面的控制器,该地址要保证和表单提交的地址一致!
.successHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest arg0, HttpServletResponse arg1, Authentication arg2)
throws IOException, ServletException {
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal != null && principal instanceof UserDetails) {
UserDetails user = (UserDetails) principal;
System.out.println("loginUser:"+user.getUsername());
//维护在session中
arg0.getSession().setAttribute("userDetail", user);
arg1.sendRedirect("/");
}
}
})
.permitAll()
.and()
.logout()
.logoutSuccessUrl("/index")
.permitAll()
.and()
.csrf() //关闭csrf 否则会出现跨域问题
.disable();
}
}