Spring Security小讲

近期在搞微服务,看到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();

	}

}

接下来让我们启动程序。
访问开放端点/index,正常访问.

在这里插入图片描述

访问/user端点

在这里插入图片描述

输入我们数据库中的 pk 123 角色是USER

在这里插入图片描述

成功返回index页面,接下来访问/user

在这里插入图片描述

可以看到认证通过访问正确的端点,那么我们接下来访问一下/admin端点。

在这里插入图片描述

因为我们的pk账户只有USER角色,而我们自定义的url-filter映射关系中要求/admin 对只有admin角色的用户开放,所以访问不到。

以上都是自己在网上查找资料,结合查看源码总结的,如果有什么不对的地方还请各位大佬留言说明下,万分感谢。

猜你喜欢

转载自blog.csdn.net/weixin_41758046/article/details/89304235
今日推荐