spring-boot-note11---springsecurity的使用

前言:

   Spring Security 是 Spring 家族中的一个安全管理框架,Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性的完整解决方案。一般来说,Web 应用的安全性包括用户认证(Authentication)和用户授权(Authorization)两个部分。用户认证指的是验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。用户授权指的是验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。

  原理就是:创建很多个filter和interceptor来进行请求的验证和拦截,以此来达到安全的效果。

下面通过一些案例演示:

一、基本案例的使用

   1、基本案例功能概述

   通过内存配置的用户名和密码,进行认证授权,另外配置链接/admin/**,需要admin的角色才可以访问,/user/**,需要user的角色才可以访问,案例的java类就一个主配置类,进行了功能的配置,因为springsecurity有默认的登陆/login,因此Controller主要创建了/admin/hello、/user/hello、/hello的三个请求方法。

  2、代码

 添加pom依赖

	<dependency>
	   		<groupId>org.springframework.boot</groupId>
	    	<artifactId>spring-boot-starter-security</artifactId>
		</dependency>
		

配置类

通过configure(AuthenticationManagerBuilder) 添加认证需要提供的用户

     例 auth.inMemoryAuthentication().withUser("admin")  .password("admin")  .roles("admin", "user") // 内存用户admin,密码admin,角色admin,user

通过configure(HttpSecurity http)进行路径对应权限的拦截以及其他的一些操作。

       and().logout().logoutUrl("/logout")  .logoutSuccessHandler(new LogoutSuccessHandlers())  // 增加自定义退出的处理。

           authorizeRequests()
          .antMatchers("/user/**").hasRole("user")  // user开头需要user的角色
           antMatchers("/admin/**").hasRole("admin") // amdin开头需要admin的角色

           and().anyRequest().authenticated() // 所有的路径需要登陆

           and().csrf().disable()  // 关闭默认的csrf功能,不然logout无法用get请求,以及其他类似的问题出现。

@Configuration
public class MySecurityConfiguration extends WebSecurityConfigurerAdapter {
	
	// 添加一个admin/admin role是 admin user  user/user role是user, guest, role 是guest 的静态用户数据
	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		auth.inMemoryAuthentication().withUser("admin")
				.password("admin") 
				.roles("admin", "user")
				.and().withUser("user") 
				.password("user").roles("user").and().withUser("guest") 
				.password("guest").roles();
	}
	
	// 配置路径的拦截 
	// produect  需要user权限
	// admin 需要admin权限
	protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers("/user/**").hasRole("user")  // user开头需要user的角色
                .antMatchers("/admin/**").hasRole("admin") // amdin开头需要admin的角色
                .anyRequest().authenticated() // 所有请求需要认证
                .and().formLogin()
                .and()
                .httpBasic()
                .and()
		        .logout() // 增加退出,指定路径是logout,退出处理结果在下面的内部类中。
		        .logoutUrl("/logout")
		        .logoutSuccessHandler(new LogoutSuccessHandlers())
		        .and().csrf().disable(); //  这里关闭csrf是为了logout生效(默认post)
    }
	
	private static class LogoutSuccessHandlers implements LogoutSuccessHandler {
		@Override
		public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response,
				Authentication authentication) throws IOException, ServletException {
			  response.setContentType("application/json;charset=utf-8");
              PrintWriter out = response.getWriter();
              out.write("退出登陆");
              out.flush();
		}
	}
	
}

Controller

 

/**
 * 如果没有配置configure(AuthenticationManagerBuilder auth),默认密码:user , 启动时随机密码
 */
@RestController
public class HelloController {

	@RequestMapping("/admin/hello")
	public String adminhello() {
		return "admin hello";
	}
	
    @RequestMapping("/user/hello")
    public String producthello(){
        return "user hello";
    }
    
    @RequestMapping("/hello")
    public String hello() {
    	return "hello";
    }
}

  测试使用

   预期:打开http://localhost:8080/hello , 会自动跳转到/login页面,输入admin/admin用户名密码,可以访问/hello、/admin/hello、/user/hello,如果密码错误,/login页面会报错,如果使用user/user的用户密码组,除了/admin/hello不能访问。   

   结果:测试符合预期

二、使用数据库存储用户案例

    我们真正使用安全框架的时候,不可能把用户名密码配置在内存中,这个时候,就是根据登陆的用户名和密码,到数据中进行查询,如果匹配,就登陆成功并加载权限等,如果不匹配,就返回错误,在shiro中我们是通过自定义realm,在doAuth方法中,根据用户名查询用户信息,返回自定义用户信息,密码,以及配置了密码的加密方式,盐等。

    在spring-security中,我们只需要提供一个实现标准接口UserDetailsService的loadUserByUsername(String username)方法,如果查询出用户就组装UserDetails接口的数据,没有抛异常,其他的就交给框架工作。使用感官上和shiro基本一致,区别:spring-security的用户和角色信息一次加载了。

  1、通过 auth.userDetailsService(userDetailsService) 配置我们自己实现的userDetailsService

	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		 auth.userDetailsService(userDetailsService)
         .passwordEncoder(new BCryptPasswordEncoder());
	}

2、userDetailsService

@Component("userDetailsService")
public class UserDetailsServiceImple implements UserDetailsService{
	
	/** security中ROLE判断的默认前缀ROLE_, 你不加,它对比的时候会加。*/
	private static final String role_prefix = "ROLE_";
	
	private UserDao userDao = new UserDao();

	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		User user = userDao.findUserByUsername(username);
		if (user == null) {
			throw new UsernameNotFoundException("用户不存在!");
		}
		Collection<GrantedAuthority> grantedAuthorities = new ArrayList<>();
		GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(role_prefix+user.getRole());
		grantedAuthorities.add(grantedAuthority);
		return new org.springframework.security.core.userdetails.User(username, user.getPassword(), grantedAuthorities);
	}
	
}

3、UserDao 这个是模拟的数据库数据,有admin/admin role=admin和user/user role=role的2个用户,另外因为我们加密使用的是spring-security的加密方式,那么数据库存的密码就需要用spring-security的加密进行处理

/**
 * 数据库操作的模拟
 */
public class UserDao {
	
	private static Map<String,User> users = new HashMap<String,User>();
	
	static {
		User user1 = new User();
		user1.setUsername("admin"); // 密码是123456采用
		user1.setPassword("$2a$10$XLO0nZFBvLguTssPZdYr1ueQeiCYztmlKmh3J5XPLVOALuXRCzVX6");
		user1.setUsername("admin");
		user1.setRole("admin");
		User user2 = new User();
		user2.setUsername("user");
		user2.setPassword("$2a$10$XLO0nZFBvLguTssPZdYr1ueQeiCYztmlKmh3J5XPLVOALuXRCzVX6");
		user2.setRole("user");
		users.put(user1.getUsername(), user1);
		users.put(user2.getUsername(), user2);
	}
	
	public User findUserByUsername(String username) {
		User user = users.get(username);
		return user;
	}
	
	/**
	 * spring-security加密的测试。
	 */
	public static void main(String[] args) {
		BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
		String password = bCryptPasswordEncoder.encode("123456");
		System.out.println(password);
	}
	
}

4、controller上我们主要增加了/center的请求方法。

    /**
     * SecurityContextHolder.getContext().getAuthentication().getPrincipal() 获取登陆用户 
     */
    @RequestMapping("/center")
	public String center() {
		String userDetails = null;
		Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
		if (principal instanceof UserDetails) {
			userDetails = ((UserDetails) principal).getUsername();
			userDetails = ((UserDetails) principal).getAuthorities().toString();
		} else {
			userDetails = principal.toString();
		}
		return userDetails;
	}

userDetails  = SecurityContextHolder.getContext().getAuthentication().getPrincipal()  即我们认证时loadUserByUsername方法访问的用户认证信息。

5、配置类MySecurityConfiguration

/**
 * Security的配置类
 */
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true) // 开启支持注解方式的权限细粒度配置
public class MySecurityConfiguration extends WebSecurityConfigurerAdapter {
		
	@Autowired
	private UserDetailsService userDetailsService;
	
	// 添加一个userDetailService通过数据库认证,并加载角色信息, 以及设置加密方法
	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		 auth.userDetailsService(userDetailsService)
         .passwordEncoder(new BCryptPasswordEncoder());
	}
	
	// 配置路径的拦截 
	// produect  需要user权限
	// admin 需要admin权限
	protected void configure(HttpSecurity http) throws Exception {
		 http
         .authorizeRequests()
         .antMatchers("/user/**").hasRole("user")
         .antMatchers("/admin/**").hasRole("admin")
         .anyRequest().authenticated() //
         .and()
         .formLogin()
         .and()
         .httpBasic()
         .and().logout().logoutUrl("/logout")
         .and().csrf().disable();
    }
	
}

  测试使用

   预期:打开http://localhost:8080/hello , 会自动跳转到/login页面,输入admin/admin用户名密码,可以访问/hello、/user/hello,如果密码错误,/login页面会报错,如果使用user/user的用户密码组,则/admin/hello不能访问。另外/center页面可以获取用户认证后的信息。

   结果:测试符合预期

三、JWT的方式

  1、主要操作

   使用jwt的方式,我们在认证这块还是一样的,即使用原有的密码用户进行登陆,但是,不使用session进行存储用户信息

 http
            .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 无状态session

  那么我们已经登陆后,怎么才能够获取用户是否登陆的这个认证后信息呢? 主要是采用一个拦截器,在默认的认证拦截器前运行,通过req.getHeader(),获取自定义的auth中带的token字符串(这个token是我们在登陆成功后,返回前端保存的)。

protected void configure(HttpSecurity http) throws Exception {
              .and().addFilterBefore(new JWTFilter(jwtService), UsernamePasswordAuthenticationFilter.class);
public class JWTFilter extends GenericFilterBean {

	private final static String HEADER_AUTH_NAME = "auth";

	private JwtService jwtService;

	public JWTFilter(JwtService jwtService) {
		this.jwtService = jwtService;
	}

	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		try {
			HttpServletRequest httpServletRequest = (HttpServletRequest) request;
			String authToken = httpServletRequest.getHeader(HEADER_AUTH_NAME);
			String parameterToken = httpServletRequest.getParameter(HEADER_AUTH_NAME);
			if(StringUtils.isNotEmpty(parameterToken)) { // 兼顾param上取token值,方便获取。
				Authentication authentication = jwtService.getAuthentication(parameterToken);
				if (authentication != null) {
					SecurityContextHolder.getContext().setAuthentication(authentication);
				}
			} else {
				if (StringUtils.isNotEmpty(authToken)) {
					Authentication authentication = jwtService.getAuthentication(authToken);
					if (authentication != null) {
						SecurityContextHolder.getContext().setAuthentication(authentication);
					}
				}
			}
			chain.doFilter(request, response);
		} catch (Exception ex) {
			throw new RuntimeException(ex);
		}
	}

}
	/**
	 * 认证成功处理
	 */
	private class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler{
		@Override
		public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
				Authentication authentication) throws IOException, ServletException {
			  PrintWriter writer = response.getWriter();
              writer.println(jwtService.createToken(authentication));
		}
	}

 2、jwt的操作类

<dependency>
			<groupId>com.auth0</groupId>
			<artifactId>java-jwt</artifactId>
			<version>3.4.0</version>
		</dependency>
public class JWTUtils {
	    
    /**
     * 私有key,加密是用户名密码的基础上再加上private_key
     */
    private static final String private_key = "abcdef";
    
    /**
     * token刷新时间 , 默认半小时
     */
    private static final long token_expire_time = 30 * 60 * 1000 ;
	
    public static String sign(String username, String secret) {
		Date date = new Date(System.currentTimeMillis() + token_expire_time);
		Algorithm algorithm = Algorithm.HMAC256(private_key + secret);
		return JWT.create().withClaim("username", username).withExpiresAt(date).sign(algorithm);
    }
    
    public static boolean verify(String token, String username, String secret) {
    	Algorithm algorithm = Algorithm.HMAC256(private_key + secret);
		JWTVerifier verifier = JWT.require(algorithm).withClaim("username", username).build();
		try {
			DecodedJWT jwt = verifier.verify(token);
		} catch (JWTVerificationException e) {
			return false;
		}
		return true;
    }

    public static String getUsername(String token) {
    	return getClaim(token,"username");
    }
    
    public static String getClaim(String token,String key) {
    	  DecodedJWT jwt = JWT.decode(token);
          return jwt.getClaim(key).asString();
    }

}
/**
 * jwt认证相关
 */
@Service
public class JwtService {
	
	private UserDao userDao = new UserDao();

    public String createToken(Authentication authentication) {
    	String username = authentication.getName(); // 实际从userDetails中获取usernmae
    	User user = userDao.findUserByUsername(username);
    	String sign = JWTUtils.sign(username, user.getPassword());
    	return sign;
    }
    
    public Authentication getAuthentication(String token) {
		String username = JWTUtils.getUsername(token);
		User user = userDao.findUserByUsername(username);
		if (user == null) {
			return null;
		}
		boolean verify = JWTUtils.verify(token, username, user.getPassword());
		if(!verify) {
			return null;
		}
		MyUser myUser = new MyUser(user);
		Collection<? extends GrantedAuthority> authorities = myUser.getAuthorities();
		return new UsernamePasswordAuthenticationToken(user, token, authorities);
    }
	
}

3、pojo和dao等,UserDao类不变、User类不变案例增加了一个MyUser的user包装类,实现了UserDetails,不是必须。

public class MyUser implements UserDetails {

	private String username;

	private String password;

	private String role;

	private static final long serialVersionUID = -601304311090873229L;

	public MyUser() {
	}

	public MyUser(User user) {
		this.username = user.getUsername();
		this.password = user.getPassword();
		this.role = user.getRole();
	}

	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		if (role != null) {
			return Collections.singleton(new SimpleGrantedAuthority("ROLE_"+this.role));
		}
		return Arrays.asList(new GrantedAuthority[0]);
	}

	@Override
	public String getPassword() {
		return this.password;
	}

	@Override
	public String getUsername() {
		return this.username;
	}

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

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

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

	@Override
	public boolean isEnabled() {
		return false;
	}

}

4、配置类

@Configuration
public class MySecurityConfiguration extends WebSecurityConfigurerAdapter {

	@Autowired
	private UserDetailsService userDetailsService;
	
	@Autowired
	private JwtService jwtService;
	
	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		 auth.userDetailsService(userDetailsService)
         .passwordEncoder(new BCryptPasswordEncoder());
	}
	
	protected void configure(HttpSecurity http) throws Exception {
        http
            .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 无状态session
            .and()
            .authorizeRequests()
            .antMatchers("/user/**").hasRole("user") 
            .antMatchers("/admin/**").hasRole("admin")
            .anyRequest().authenticated() 
            .and()
        	// 配置登陆和登陆成功的处理
            .formLogin().loginProcessingUrl("/login").successHandler(this.new MyAuthenticationSuccessHandler())
            .and().csrf().disable() 
            .httpBasic()
        	// 配置登出和登出处理器
            .and()
            .logout()
	        .logoutUrl("/logout")
	        .logoutSuccessHandler(new LogoutSuccessHandlers())
        	// 在UsernamePasswordAuthenticationFilter之前执行我们添加的JWTFilter
            .and().addFilterBefore(new JWTFilter(jwtService), UsernamePasswordAuthenticationFilter.class);
    }
	
	@Override
    public void configure(WebSecurity web) {
        web.ignoring()
            .antMatchers("/swagger-resources/**")
            .antMatchers("/swagger-ui.html")
            .antMatchers("/webjars/**")
            .antMatchers("/v2/**")
            .antMatchers("/h2-console/**");
    }
	
	/**
	 * 认证成功处理
	 */
	private class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler{
		@Override
		public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
				Authentication authentication) throws IOException, ServletException {
			  PrintWriter writer = response.getWriter();
              writer.println(jwtService.createToken(authentication));
		}
	}
	
	/**
	 * 退出处理
	 */
	private static class LogoutSuccessHandlers implements LogoutSuccessHandler {
		@Override
		public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response,
				Authentication authentication) throws IOException, ServletException {
			  response.setContentType("application/json;charset=utf-8");
              PrintWriter out = response.getWriter();
              out.write("退出登陆");
              out.flush();
		}
	}
	
}

5、测试使用

   预期:用admin/123456登陆,登陆成功后,会返回token信息,然后带上token,依次访问/hello?auth=token   /admin/hello?auth=token  /user/hello?auth=token,预期admin用户不能访问/user/hello。

   结果:测试符合预期

四、 注解

@EnableGlobalMethodSecurity(prePostEnabled = true)   

 //hasRole和hasAuthority都会对UserDetails中的getAuthorities进行判断区别是hasRole会对字段加
    // 上ROLE_后再进行判断
    @RequestMapping("/test")
    @PreAuthorize("hasRole('admin')") // 需要admin的角色
   // @PreAuthorize("hasAuthority('ROLE_admin')") //  
    public String test() {
    	return "id";
    }

五、spring-security主要的介绍

	SecurityContext :安全的上下文,所有的数据都是保存到SecurityContext中,通过SecurityContext获取Authentication。
	SecurityContextHolder:用来获取SecurityContext中保存的数据的工具 SecurityContext context = SecurityContextHolder.getContext();
	Authentication:表示当前的认证情况,如UserDetails、Credentials、isAuthenticated、Principal。
		UserDetails:getPassword、getUsername等
	UserDetailsService:用来自己自定义查询UserDetails信息
	AuthenticationManager:AuthenticationManager用来进行验证,如果验证失败会抛出相对应的异常
	PasswordEncoder: 密码加密器
		BCryptPasswordEncoder:哈希算法加密
		NoOpPasswordEncoder:不使用加密

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值