SpringSecurity - 图1

1、简介:

1.1、安全框架概述

  • 什么是安全框架?
    • 解决系统安全问题的框架。
  • 如果没有安全框架
    • 需要手动处理(手动编写servlet每个资源的访问控制,非常麻烦)。
  • 使用安全框架
    • 可以通过配置的方式实现对资源的访问限制。

1.2、常用安全框架

Spring Security:

  • 基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。
  • 提供了一组可以在Spring应用上下文中配置的Bean,充分利用了 Spring IOC , DI(控制反转Inversion of Control,DI:Dependency Injection 依赖注入)和AOP(面向切面编程) 功能。
  • 对于authentication和authorization,Spring Security 框架都有很好的支持。
    • 在用户认证(authentication)方面,Spring Security 框架支持主流的认证方式,包括 HTTP 基本认证、HTTP 表单验证、HTTP 摘要认证、OpenID 和 LDAP 等。
    • 在用户授权方面(authorization),Spring Security 提供了基于角色的访问控制和访问控制列表(Access Control List,ACL),可以对应用中的领域对象进行细粒度的控制。

Apache Shiro:

  • 一个功能强大且易于使用的Java安全框架,提供了认证,授权,加密,和会话管理。
  • 可定制化
  • 认证(authentication)和授权(authorization)

SpringSecurity - 图2

1.3、概述

  1. **Spring Security是一个高度自定义的安全框架。利用 Spring IOC/DIAOP功能,为系统提供了声明式安全访问控制功能,减少了为系统安全而编写大量重复代码的工作。**
  1. **使用 Spring Secruity 的原因有 很多,但大部分都是发现了 javaEE Servlet 规范或 EJB 规范中的安全功能缺乏典型企业应用场景。同时认识到它们在 WAR EAR 级别无法移植。因此如果你更换服务器环境,还有大量工作去重新配置应用程序。使用 Spring Security解决了这些问题,也为你提供许多其他有用的、可定制的安全功能。 正如你可能知道的两个应用程序的两个主要区域是==“认证”****和****“授权”==(或者访问控制)。**
  2. **这两点也是 Spring Security 重要核心功能。**
  • “认证”,是建立一个声明主体的过程(一个“主体”一般是指用户, 设备或一些可以在应用程序中执行动作的其他系统),通俗点说就是系统认为用户是否能登录。
  • “授权”指确定一个主体是否允许在你的应用程序执行一个动作的过程。通俗点讲就是系统判断用户是否 有权限去做某些事情。

2、Spring Security

SpringSecurity - 图3

2.1、快速入门

pom.xml

  1. <!--SpringSecurity组件-->
  2. <dependency>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-starter-security</artifactId>
  5. </dependency>
  6. <!--web组件-->
  7. <dependency>
  8. <groupId>org.springframework.boot</groupId>
  9. <artifactId>spring-boot-starter-web</artifactId>
  10. </dependency>
  11. <!--test组件-->
  12. <dependency>
  13. <groupId>org.springframework.boot</groupId>
  14. <artifactId>spring-boot-starter-test</artifactId>
  15. <scope>test</scope>
  16. </dependency>
  17. <dependency>
  18. <groupId>org.springframework.security</groupId>
  19. <artifactId>spring-security-test</artifactId>
  20. <scope>test</scope>
  21. </dependency>

login.html、main.html

首页

controller

  1. @RequestMapping("/login",method = RequestMethod.POST)
  2. public String login() {
  3. return "redirect:main.html";
  4. }

注意:

  • 不自定义配置下,它跳转的是spring security的默认登录界面
  • 用户名 默认user,密码自动生成

2.2、UserDetailsService

  1. public interface UserDetailsService {
  2. //通过用户名先认证 数据库没有此用户会抛出异常 有则跳转到UserDetails进一步认证
  3. UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
  4. }
  5. public interface UserDetails extends Serializable {
  6. Collection<? extends GrantedAuthority> getAuthorities();
  7. String getPassword();//密码
  8. String getUsername();//用户名
  9. boolean isAccountNonExpired();//帐户未过期
  10. boolean isAccountNonLocked();//帐户未锁定
  11. boolean isCredentialsNonExpired();//凭证未过期
  12. boolean isEnabled();//能够使用
  13. }
  14. //UserDetails是接口,要找具体实现类User
  15. public class User implements UserDetails, CredentialsContainer {
  16. private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
  17. private static final Log logger = LogFactory.getLog(User.class);
  18. private String password;//密码
  19. private final String username;//用户名
  20. private final Set<GrantedAuthority> authorities;//授权
  21. private final boolean accountNonExpired;//帐户未过期
  22. private final boolean accountNonLocked;//帐户未锁定
  23. private final boolean credentialsNonExpired;//凭证未过期
  24. private final boolean enabled;//能够使用
  25. /*构造器
  26. 三个参数 username password authorities 用户具有的权限。此处不允许为 null
  27. */
  28. public User(String username, String password,
  29. Collection<? extends GrantedAuthority> authorities) {
  30. this(username, password, true, true, true, true, authorities);
  31. }
  32. /*构造器重载
  33. 七个参数:username password
  34. enabled accountNonExpired
  35. credentialsNonExpired accountNonLocked
  36. authorities 用户具有的权限。此处不允许为 null
  37. */
  38. public User(String username, String password,
  39. boolean enabled, boolean accountNonExpired,
  40. boolean credentialsNonExpired, boolean accountNonLocked,
  41. Collection<? extends GrantedAuthority> authorities) {
  42. Assert.isTrue(username != null && !"".equals(username) && password != null,
  43. "Cannot pass null or empty values to constructor");
  44. this.username = username;
  45. this.password = password;
  46. this.enabled = enabled;
  47. this.accountNonExpired = accountNonExpired;
  48. this.credentialsNonExpired = credentialsNonExpired;
  49. this.accountNonLocked = accountNonLocked;
  50. this.authorities = Collections.unmodifiableSet(sortAuthorities(authorities));
  51. }
  52. }
  53. /*此处的用户名应该是客户端传递过来的用户名。而密码应该是从数据库中查询出来的密码。
  54. SpringSecurity 会根据 User 中的 password 和客户端传递过来的 password 进行比较。
  55. 如果相同则表示认证通过,如果不相同表示认证失败
  56. */
  • authorities 里面的权限对于后面学习授权是很有必要的,包含的所有内容为此用户具有的权限, 如有里面没有包含某个权限,而在做某个事情时必须包含某个权限则会出现 403。
  • 通常都是通过AuthorityUtils.commaSeparatedStringToAuthorityList(“”) 来创建 authorities 集合对象 的。参数是一个字符串,多个权限使用逗号分隔。
  • 方法参数 方法参数表示用户名。
    • 此值是客户端表单传递过来的数据。默认情况下必须叫 username ,否则无法接收

2.3、PasswordEncoder 密码解析器详解

源码:

  1. public interface PasswordEncoder {
  2. /**
  3. * 加密.
  4. * SHA-1
  5. * or greater hash combined with an 8-byte 哈希
  6. or greater randomly generated salt. 盐值处理
  7. */
  8. String encode(CharSequence rawPassword);
  9. /*验证从存储中获取的编码密码与编码后提交的原始密码是否匹配。
  10. 如果密码匹配,则返回 true;
  11. 如果不匹配,则返回 false。
  12. 第一个参数表示需要被解析的密码。第二个参数表示存储的密码。
  13. */
  14. boolean matches(CharSequence rawPassword, String encodedPassword);
  15. //可二次加密 返回true
  16. default boolean upgradeEncoding(String encodedPassword) {
  17. return false;
  18. }
  19. }
  20. //多个实现类 推荐使用BCryptPasswordEncoder
  21. public class BCryptPasswordEncoder implements PasswordEncoder {
  22. private Pattern BCRYPT_PATTERN = Pattern.compile("\\A\\$2(a|y|b)?\\$(\\d\\d)\\$[./0-9A-Za-z]{53}");
  23. private final Log logger = LogFactory.getLog(getClass());
  24. private final int strength;//默认10
  25. private final BCryptVersion version;
  26. private final SecureRandom random;
  27. //枚举类型版本号
  28. public enum BCryptVersion {
  29. $2A("$2a"),
  30. $2Y("$2y"),
  31. $2B("$2b");
  32. private final String version;
  33. BCryptVersion(String version) {
  34. this.version = version;
  35. }
  36. public String getVersion() {
  37. return this.version;
  38. }
  39. }
  40. }

构造器:

  1. /**
  2. * 三个参数:
  3. * BCryptVersion version,//版本号
  4. * int strength, //长度
  5. * SecureRandom random//随机数
  6. * 方法重载
  7. */
  8. public BCryptPasswordEncoder() {
  9. this(-1);
  10. }
  11. public BCryptPasswordEncoder(int strength) {
  12. this(strength, null);
  13. }
  14. public BCryptPasswordEncoder(BCryptVersion version) {
  15. this(version, null);
  16. }
  17. public BCryptPasswordEncoder(BCryptVersion version, SecureRandom random) {
  18. this(version, -1, random);
  19. }
  20. public BCryptPasswordEncoder(int strength, SecureRandom random) {
  21. this(BCryptVersion.$2A, strength, random);
  22. }
  23. public BCryptPasswordEncoder(BCryptVersion version, int strength) {
  24. this(version, strength, null);
  25. }
  26. public BCryptPasswordEncoder(BCryptVersion version, int strength, SecureRandom random) {
  27. if (strength != -1 && (strength < BCrypt.MIN_LOG_ROUNDS || strength > BCrypt.MAX_LOG_ROUNDS)) {
  28. throw new IllegalArgumentException("Bad strength");
  29. }
  30. this.version = version;
  31. this.strength = (strength == -1) ? 10 : strength;
  32. this.random = random;
  33. }
  34. }

加密:

  1. //加密 盐值 随机数 保证相同密码每次加密结果不一样
  2. @Override
  3. public String encode(CharSequence rawPassword) {
  4. if (rawPassword == null) {
  5. throw new IllegalArgumentException("rawPassword cannot be null");
  6. }
  7. String salt = getSalt();
  8. return BCrypt.hashpw(rawPassword.toString(), salt);
  9. }
  10. //获取盐值
  11. private String getSalt() {
  12. if (this.random != null) {
  13. return BCrypt.gensalt(this.version.getVersion(), this.strength, this.random);
  14. }
  15. return BCrypt.gensalt(this.version.getVersion(), this.strength);
  16. }

密码匹配:

  1. @Override
  2. public boolean matches(CharSequence rawPassword, String encodedPassword) {
  3. if (rawPassword == null) {
  4. throw new IllegalArgumentException("rawPassword cannot be null");
  5. }
  6. if (encodedPassword == null || encodedPassword.length() == 0) {
  7. this.logger.warn("Empty encoded password");
  8. return false;
  9. }
  10. if (!this.BCRYPT_PATTERN.matcher(encodedPassword).matches()) {
  11. this.logger.warn("Encoded password does not look like BCrypt");
  12. return false;
  13. }
  14. return BCrypt.checkpw(rawPassword.toString(), encodedPassword);
  15. }

二次加密:

  1. //二次加密
  2. @Override
  3. public boolean upgradeEncoding(String encodedPassword) {
  4. if (encodedPassword == null || encodedPassword.length() == 0) {
  5. this.logger.warn("Empty encoded password");
  6. return false;
  7. }
  8. Matcher matcher = this.BCRYPT_PATTERN.matcher(encodedPassword);
  9. if (!matcher.matches()) {
  10. throw new IllegalArgumentException("Encoded password does not look like BCrypt: " + encodedPassword);
  11. }
  12. int strength = Integer.parseInt(matcher.group(2));
  13. return strength < this.strength;
  14. }

测试:

  1. @Test
  2. public void contextLoads() {
  3. PasswordEncoder encoder=new BCryptPasswordEncoder();
  4. String encode = encoder.encode("123");
  5. System.out.println(encode);
  6. boolean matches = encoder.matches("123", encode);
  7. System.out.println("===========================");
  8. System.out.println(matches);
  9. }

2.4、自定义登录逻辑

  • 当进行自定义登录逻辑时需用到UserDetailsServicePasswordEncoder
  • 但是 Spring Security 要求:当进行自定义登录逻辑时容器内必须有 PasswordEncoder实例。
  • 不能直接 new 对象。

2.4.1、登录常用参数

登录成功或失败

  1. formLogin() 基于表单登录
  2. loginPage() 登录页
  3. defaultSuccessUrl 登录成功后的默认处理页
  4. failuerHandler 登录失败之后的处理器
  5. successHandler 登录成功之后的处理器
  6. failuerUrl 登录失败之后系统转向的url,默认是this.loginPage + "?error"

注销

  1. logoutUrl 登出url,默认是/logout 它可以是一个ant path url
  2. logoutSuccessUrl 登出成功后跳转的 url 默认是"/login?logout"
  3. logoutSuccessHandler 登出成功处理器,设置后会把logoutSuccessUrl 置为null

2.4.2、编写配置类:

  1. @Configuration
  2. public class SecurityConfig {
  3. @Bean
  4. public PasswordEncoder getPw(){
  5. return new BCryptPasswordEncoder();
  6. }
  7. }

2.4.3、自定义逻辑

  1. @Service
  2. public class UserDetailsServiceImpl implements UserDetailsService {
  3. @Autowired
  4. private PasswordEncoder pw;
  5. @Override
  6. public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
  7. System.out.println("执行了loadUserByUsername方法");
  8. //1.查询数据库判断用户名是否存在,如果不存在就会抛出UsernameNotFoundException异常
  9. if (!"admin".equals(username)) {
  10. throw new UsernameNotFoundException("用户名不存在");
  11. }
  12. //2.把查询出来的密码(注册时已经加密过)进行解析,或者直接把密码放入构造方法
  13. String password = pw.encode("root");
  14. return new User(username,password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin,normal"));
  15. }
  16. }

2.4.4、自定义登录界面

  1. Spring Security 提供了登录页面,但是对于实际项目中,会自定义登录页面。所以 Spring Security 中不仅仅提供了登录页面,还支持用户自定义登录页面。实现过程也比较简单,只需要修改配置类即可。

修改配置类中主要是设置哪个页面是登录页面。配置类需要继承WebSecurityConfigurerAdapter,并重写configure 方法。

  • successForwardUrl() :登录成功后跳转地址 loginPage() :
  • loginProcessingUrl :登录页面表单提交地址,此地址可以不真实存在。
  • antMatchers() :匹配内容
  • permitAll() :允许所有
  1. @Override
  2. protected void configure(HttpSecurity http) throws Exception {
  3. //自定义登录页面
  4. http.formLogin()
  5. .loginPage("/login.html")
  6. .loginProcessingUrl("/login")
  7. //登录成功后跳转页面,POST请求
  8. .successForwardUrl("/toMain");
  9. //授权认证
  10. http.authorizeRequests()
  11. //login.html不需要被认证
  12. .antMatchers("/login.html").permitAll()
  13. //所有请求都必须被认证,必须登录后被访问
  14. .anyRequest().authenticated();
  15. //关闭csrf防护关闭 跨站请求伪造
  16. http.csrf().disable();
  17. }

修改controller

  1. @RequestMapping(value = "/toMain",method = RequestMethod.POST)
  2. public String toMain() {
  3. return "redirect:main.html";
  4. }

登陆失败

controller

  1. @RequestMapping("/toError",method = RequestMethod.POST)
  2. public String toError() {
  3. return "redirect:error.html";
  4. }

修改配置类

  1. http.formLogin().failureForwardUrl("/toError");
  2. //放行toError
  3. http.authorizeRequests().antMatchers("/error.html").permitAll();

error.hmtl
跳转

2.4.5、自定义入参

源码

  1. public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
  2. public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
  3. public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
  4. private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/login",
  5. "POST");
  6. private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
  7. private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
  8. private boolean postOnly = true;
  9. public UsernamePasswordAuthenticationFilter() {
  10. super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
  11. }
  12. public UsernamePasswordAuthenticationFilter(AuthenticationManager authenticationManager) {
  13. super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager);
  14. }
  15. @Override
  16. public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
  17. throws AuthenticationException {
  18. if (this.postOnly && !request.getMethod().equals("POST")) {
  19. throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
  20. }
  21. String username = obtainUsername(request);
  22. username = (username != null) ? username : "";
  23. username = username.trim();
  24. String password = obtainPassword(request);
  25. password = (password != null) ? password : "";
  26. UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
  27. // Allow subclasses to set the "details" property
  28. setDetails(request, authRequest);
  29. return this.getAuthenticationManager().authenticate(authRequest);
  30. }
  31. @Nullable
  32. protected String obtainPassword(HttpServletRequest request) {
  33. return request.getParameter(this.passwordParameter);
  34. }
  35. @Nullable
  36. protected String obtainUsername(HttpServletRequest request) {
  37. return request.getParameter(this.usernameParameter);
  38. }
  39. protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) {
  40. authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
  41. }
  42. public void setUsernameParameter(String usernameParameter) {
  43. Assert.hasText(usernameParameter, "Username parameter must not be empty or null");
  44. this.usernameParameter = usernameParameter;
  45. }
  46. public void setPasswordParameter(String passwordParameter) {
  47. Assert.hasText(passwordParameter, "Password parameter must not be empty or null");
  48. this.passwordParameter = passwordParameter;
  49. }
  50. public void setPostOnly(boolean postOnly) {
  51. this.postOnly = postOnly;
  52. }
  53. public final String getUsernameParameter() {
  54. return this.usernameParameter;
  55. }
  56. public final String getPasswordParameter() {
  57. return this.passwordParameter;
  58. }
  59. }
  • public static final String SPRING_SECURITY_FORM_USERNAME_KEY = “username”;
  • public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = “password”;
  1. http.formLogin()
  2. //自定义入参
  3. .usernameParameter("uname")
  4. .passwordParameter("pword")

2.4.6、设置登录处理器

登录成功

源码分析

AuthenticationSuccessHandler

成功处理器接口 ——> 实现类ForwardAuthenticationSuccessHandler

  1. public interface AuthenticationSuccessHandler {
  2. default void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
  3. Authentication authentication) throws IOException, ServletException {
  4. onAuthenticationSuccess(request, response, authentication);
  5. //放行
  6. chain.doFilter(request, response);
  7. }
  8. void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
  9. Authentication authentication) throws IOException, ServletException;
  10. }
  11. public class ForwardAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
  12. private final String forwardUrl;
  13. public ForwardAuthenticationSuccessHandler(String forwardUrl) {
  14. Assert.isTrue(UrlUtils.isValidRedirectUrl(forwardUrl), () -> "'" + forwardUrl + "' is not a valid forward URL");
  15. this.forwardUrl = forwardUrl;
  16. }
  17. @Override
  18. public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
  19. Authentication authentication) throws IOException, ServletException {
  20. //转发 不能跳转外部资源,不适用前后端分离项目
  21. request.getRequestDispatcher(this.forwardUrl).forward(request, response);
  22. }
  23. }
  • ForwardAuthenticationSuccessHandler内部就是最简单的请求转发。
  • 由于是请求转发,当遇到需要跳转到站外或在前后端分离的项目中就无法使用了。
  1. public interface Authentication extends Principal, Serializable {
  2. Collection<? extends GrantedAuthority> getAuthorities();//获取拥有权限的人
  3. Object getCredentials();//获取密码
  4. Object getDetails();
  5. Object getPrincipal();//获取用户信息
  6. boolean isAuthenticated();
  7. void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
  8. }

自定义处理器

  1. public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
  2. private String url;
  3. public MyAuthenticationSuccessHandler(String url) {
  4. this.url = url;
  5. }
  6. @Override
  7. public void onAuthenticationSuccess(HttpServletRequest request,
  8. HttpServletResponse response,
  9. Authentication authentication) throws IOException,ServletException {
  10. //Principal 主体,存放了登录用户的信息
  11. User user = (User) authentication.getPrincipal();
  12. System.out.println(user.getUsername());
  13. //保密 输出null
  14. System.out.println(user.getPassword());
  15. System.out.println(user.getAuthorities());
  16. //重定向url
  17. response.sendRedirect(url);
  18. }
  19. }

修改配置类

  1. http.formLogin()
  2. //登录成功后的处理器,不能和successForwardUrl共存
  3. .successHandler(new MyAuthenticationSuccessHandler("/login.html")

登录失败

源码:

AuthenticationFailureHandler

  1. public interface AuthenticationFailureHandler {
  2. void onAuthenticationFailure(HttpServletRequest request,
  3. HttpServletResponse response,
  4. AuthenticationException exception)
  5. hrows IOException, ServletException;
  6. }

实现类:ForwardAuthenticationFailureHandler

  1. public class ForwardAuthenticationFailureHandler implements AuthenticationFailureHandler {
  2. private final String forwardUrl;
  3. public ForwardAuthenticationFailureHandler(String forwardUrl) {
  4. Assert.isTrue(UrlUtils.isValidRedirectUrl(forwardUrl), () -> "'" + forwardUrl + "' is not a valid forward URL");
  5. this.forwardUrl = forwardUrl;
  6. }
  7. @Override
  8. public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
  9. AuthenticationException exception) throws IOException, ServletException {
  10. request.setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, exception);
  11. //转发
  12. request.getRequestDispatcher(this.forwardUrl).forward(request, response);
  13. }
  14. }

自定义处理器

  1. public class MyFailureHandler implements AuthenticationFailureHandler {
  2. private String url;
  3. public MyFailureHandler(String url) {
  4. this.url = url;
  5. }
  6. @Override
  7. public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
  8. response.sendRedirect(url);
  9. }
  10. }

修改配置类

  1. http.formLogin()
  2. //登录失败后的处理器,不能和failureForwardUrl共存
  3. .failureHandler(new MyAuthenticationFailurHandler("/error.html"))

2.4.7、访问控制url匹配

  • 在前面认证中所有常用配置,主要是对 http.formLogin() 进行操作。
  • 而在配置类中 http.authorizeRequests() 主要是对url进行控制,也就是授权(访问控制)。
  • http.authorizeRequests() 也支持连缀写法,总体公式为:
    • url 匹配规则.权限控制方法 通过上面的公式可以有很多 url 匹配规则和很多权限控制方法。
    • 这些内容进行各种组合就形成了 Spring Security中的授权。
  1. requestMatchers() 配置一个request Mather数组,参数为RequestMatcher 对象,其match 规则自定义,需要的时候放在最前面,对需要匹配的的规则进行自定义与过滤
  2. authorizeRequests() URL权限配置
  3. antMatchers() 配置一个request Mather string数组,参数为 ant 路径格式, 直接匹配url
  4. anyRequest() 匹配任意url、无参,最好放在最后面

anyRequest()
  • 任何请求,必须放在最后,有严格的执行顺序
  1. //所有请求必须认证之后才能访问
  2. http.authorizeRequests()
  3. .anyRequest().authenticated();

antMatcher()

常用作静态资源放行

  1. antMatchers(String... antPatterns)
  2. antMatchers(HttpMethod method, String... antPatterns)
  • 参数是不定向参数,每个参数是一个 ant 表达式,用于匹配 URL规则。
  • 规则如下:
    • ? : 匹配一个字符
    • *:匹配 0 个或多个字符
    • ** :匹配 0 个或多个目录

在实际项目中经常需要放行所有静态资源

  1. http.authorizeRequests()
  2. //放行相关目录下静态资源
  3. .antMatchers("/js/**","/image/**","/css/**").permitAll()
  4. //放行所有静态资源
  5. .antMatchers("/**").permitAll()

regexMatchers()
  • 正则表达式
    1. regexMatchers(String... regexPatterns)
    2. //默认匹配所有方法,也可手动定义
    3. regexMatchers(HttpMethod method, String... regexPatterns)
  • Controller
    1. @RequestMapping(value = "/demo",method = RequestMethod.GET)
    2. @ResponseBody
    3. public String demo() {
    4. return "demo";
    5. }
  • 新增配置
    1. http.authorizeRequests()
    2. .regexMatchers(".+[.]jpg").permitAll()
    3. .regexMatchers(".+[.]css").permitAll()
    4. //匹配方法
    5. .regexMatchers(HttpMethod.GET,"/demo").permitAll()

mvcMatchers()
  • servletPath 就是所有的 URL 的统一前缀。
  • 在 SpringBoot 整合SpringMVC 的项目中可以在 application.properties 中添加下面内容设置 ServletPath
  • servletPath(项目目录) mvcMatchers独有

常用形式

  1. mvcMatchers(HttpMethod method, String... mvcPatterns)
  2. mvcMatchers(String... patterns)

新增配置

  1. http.authorizeRequests()
  2. .mvcMatchers("/demo").servletPath("/xxx").permitAll()
  3. //也可写成,等效
  4. .antMatchers("/xxx/demo").permitAll()

application.properties 中新增

  1. spring.mvc.servlet.path=/xxx

2.4.8、内置访问控制方法

Spring Security 匹配了 URL 后调用了 permitAll() 表示不需要认证,随意访问。

在 Spring Security 中提供了6种内置控制。

  1. static final String permitAll = "permitAll"; //允许所有
  2. private static final String denyAll = "denyAll"; //禁止访问
  3. private static final String anonymous = "anonymous"; //匿名访问
  4. private static final String authenticated = "authenticated"; //认证访问
  5. private static final String fullyAuthenticated = "fullyAuthenticated"; //输账号密码才能进入
  6. private static final String rememberMe = "rememberMe"; //记住我

permitAll()
  • permitAll()表示所匹配的 URL 任何人都允许访问。
    1. public ExpressionInterceptUrlRegistry permitAll() {
    2. return access(permitAll);
    3. }

authenticated()
  • authenticated()表示所匹配的 URL 都需要被认证才能访问。
    1. public ExpressionInterceptUrlRegistry authenticated() {
    2. return access(authenticated);
    3. }

anonymous()
  • anonymous()表示可以匿名访问匹配的URL。和permitAll()效果类似,只是设置为 anonymous()的 url 会执行 filter 链中
    1. public ExpressionInterceptUrlRegistry rememberMe() {
    2. return access(rememberMe);
    3. }

denyAll()
  • denyAll()表示所匹配的 URL 都不允许被访问
    1. public ExpressionInterceptUrlRegistry denyAll() {
    2. return access(denyAll);
    3. }

rememberMe()
  • 被“remember me”的用户允许访问

fullyAuthenticated()
  • 如果用户不是被 remember me 的,才可以访问。
  • 必须手动输入账号密码
    1. public ExpressionInterceptUrlRegistry fullyAuthenticated() {
    2. return access(fullyAuthenticated);
    3. }

2.4.9、角色权限判断

除了之前的内置权限控制。Spring Security 中还支持很多其他权限控制。这些方法一般都用于用户已经被认证后,判断用户是否具有特定的要求。

基于权限控制

源码

  1. //添加单个角色
  2. public ExpressionInterceptUrlRegistry hasAuthority(String authority) {
  3. return this.access(ExpressionUrlAuthorizationConfigurer.hasAuthority(authority));
  4. }
  5. //添加多个角色
  6. public ExpressionInterceptUrlRegistry hasAnyAuthority(String... authorities) {
  7. return access(ExpressionUrlAuthorizationConfigurer.hasAnyAuthority(authorities));
  8. }
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>main1</title>
  6. </head>
  7. <body>
  8. <h3>main1</h3>
  9. </body>
  10. </html>

新增配置

  1. http.authorizeRequests()
  2. //一个权限
  3. //.antMatchers("/main1.html").hasAuthority("admin")
  4. //多个权限
  5. .antMatchers("/main1.html").hasAnyAuthority("admin","admad")

基于角色控制
  • 如果用户具备给定角色就允许访问。否则出现 403。
  • 参数取值来源于自定义登录逻辑 UserDetailsService 实现类中创建 User 对象时给 User 赋予的授权。
  • 在给用户赋予角色时角色需要以: ROLE_开头 ,后面添加角色名称。
  • 例如:ROLEabc 其中 abc 是角色名,ROLE是固定的字符开头。 使用 hasRole()时参数也只写 abc 即可。否则启动报错。

源码

  1. public ExpressionInterceptUrlRegistry hasRole(String role) {
  2. return access(ExpressionUrlAuthorizationConfigurer.hasRole(role));
  3. }
  4. public ExpressionInterceptUrlRegistry hasAnyRole(String... roles) {
  5. return access(ExpressionUrlAuthorizationConfigurer.hasAnyRole(roles));
  6. }

新增配置类

  1. http.authorizeRequests()
  2. //基于角色
  3. .antMatchers("/main1.html").hasRole("abc")
  4. .antMatchers("/main1.html").hasAnyRole("abc","asd")

service

  1. return new User(username,password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin,normal,ROLE_abc"));

基于IP地址控制
  • hasIpAddress(String) 如果请求是指定的 IP 就运行访问。
  • 可以通过 request.getRemoteAddr() 获取 ip 地址。
  • 需要注意的是在本机进行测试时 localhost 和 127.0.0.1 输出的 ip地址是不一样的。

源码:

  1. public ExpressionInterceptUrlRegistry hasIpAddress(String ipaddressExpression) {
  2. return access(ExpressionUrlAuthorizationConfigurer.hasIpAddress(ipaddressExpression));
  3. }

新增配置类

  1. http.authorizeRequests()
  2. .antMatchers("/main1.html").hasIpAddress("127.0.0.1")

2.4.10、自定义403处理方案

SpringSecurity - 图4

新建类实现 AccessDeniedHandler

  1. @Component
  2. public class MyAccessDeniedHandler implements AccessDeniedHandler {
  3. @Override
  4. public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
  5. //设置响应状态码
  6. httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);
  7. //设置响应头
  8. httpServletResponse.setHeader("Content-Type","application/json;charset=utf-8");
  9. PrintWriter writer = httpServletResponse.getWriter();
  10. String message = "{\"status\":\"error\",\"msg\":\"权限不足,请联系管理员\"}";
  11. writer.write(message);
  12. writer.flush();
  13. writer.close();
  14. }
  15. }

修改配置类

  1. //异常处理
  2. http.exceptionHandling()
  3. .accessDeniedHandler(myAccessDeniedHandler);

2.4.11、基于表达式的访问控制

1、access()方法使用
  1. **之前登录用户权限判断实际上底层实现都是调用access(表达式)**,**可以通过 access() 实现和之前学习的权限控制完成相同的功能。**

官网:https://docs.spring.io/spring-security/site/docs/5.2.0.RELEASE/reference/html5/

取值:

hasRole(String role) Returns true
if the current principal has the specified role.For example, hasRole('admin')
By default if the supplied role does not start with ‘ROLE_’ it will be added. This can be customized by modifying the defaultRolePrefix
on DefaultWebSecurityExpressionHandler
.
hasAnyRole(String… roles) Returns true
if the current principal has any of the supplied roles (given as a comma-separated list of strings).For example, hasAnyRole('admin', 'user')
By default if the supplied role does not start with ‘ROLE_’ it will be added. This can be customized by modifying the defaultRolePrefix
on DefaultWebSecurityExpressionHandler
.
hasAuthority(String authority) Returns true
if the current principal has the specified authority.For example, hasAuthority('read')
hasAnyAuthority(String… authorities) Returns true
if the current principal has any of the supplied authorities (given as a comma-separated list of strings)For example, hasAnyAuthority('read', 'write')
principal Allows direct access to the principal object representing the current user
authentication Allows direct access to the current Authentication
object obtained from the SecurityContext
permitAll Always evaluates to true
denyAll Always evaluates to false
isAnonymous() Returns true
if the current principal is an anonymous user
isRememberMe() Returns true
if the current principal is a remember-me user
isAuthenticated() Returns true
if the user is not anonymous
isFullyAuthenticated() Returns true
if the user is not an anonymous or a remember-me user
hasPermission(Object target, Object permission) Returns true
if the user has access to the provided target for the given permission. For example, hasPermission(domainObject, 'read')
hasPermission(Object targetId, String targetType, Object permission) Returns true
if the user has access to the provided target for the given permission. For example, hasPermission(1, 'com.example.domain.Message', 'read')

源码:
  1. public ExpressionInterceptUrlRegistry access(String attribute) {
  2. if (this.not) {
  3. attribute = "!" + attribute;
  4. }
  5. interceptUrl(this.requestMatchers, SecurityConfig.createList(attribute));
  6. return ExpressionUrlAuthorizationConfigurer.this.REGISTRY;
  7. }

修改配置类:
  1. http.authorizeRequests()
  2. .antMatchers("/login.html").access("permitAll()")
  3. .antMatchers("/login.html").access("permitAll")
  4. .antMatchers("/main1.html").access("hasRole('abc')")

2、自定义assert
  • 虽然这里面已经包含了很多的表达式(方法)但是在实际项目中很有可能出现需要自定义逻辑的情况。
  • 判断登录用户是否具有访问当前 URL 权限。

service及其实现类
  1. public interface MyService {
  2. boolean hasPermission(HttpServletRequest request, Authentication
  3. authentication);
  4. }
  5. @Service
  6. public class MyServiceImpl implements MyService {
  7. @Override
  8. public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
  9. //获取主体
  10. Object o1 = authentication.getPrincipal();
  11. if (o1 instanceof UserDetails){
  12. //强转
  13. UserDetails userDetails = (UserDetails) o1;
  14. //获取权限
  15. Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();
  16. //判断当前uri是否有权限
  17. return authorities.contains(new SimpleGrantedAuthority(request.getRequestURI()));
  18. }
  19. return false;
  20. }
  21. }

修改配置类
  1. http.authorizeRequests()
  2. //.anyRequest().authenticated();
  3. .anyRequest().access("@myServiceImpl.hasPermission(request,authentication)");

access 中通过@bean的id名.方法(参数)的形式进行操作

3、注解开发

  • 在 Spring Security 中提供了一些访问控制的注解。这些注解都是默认是都不可用的,需要通过 @EnableGlobalMethodSecurity 进行开启后使用。
  • 如果设置的条件允许,程序正常执行。如果不允许会报 500
  • 这些注解可以写到 Service 接口或方法上,也可以写到 Controller或 Controller 的方法上。
  • 通常情况下 都是写在控制器方法上的,控制接口URL是否允许被访问。

**prePostEnabled** 确定 前置注解[@PreAuthorize,@PostAuthorize,..] 是否启用

**securedEnabled** 确定安全注解 [@Secured] 是否启用

**jsr250Enabled** 确定 JSR-250注解 [@RolesAllowed..]是否启用

3.1、@Secured

@Secured 是专门用于判断是否具有角色的。能写在方法或类上。参数要以 ROLE_开头。

主启动类

  1. @EnableGlobalMethodSecurity(securedEnabled = true)

配置类

  1. @Override
  2. protected void configure(HttpSecurity http) throws Exception {
  3. //表单提交
  4. http.formLogin()
  5. //自定义登录页面
  6. .loginPage("/login.html")
  7. //当发现/login时认为是登录,必须和表单提交的地址一样。去执行UserServiceImpl
  8. .loginProcessingUrl("/login")
  9. //登录成功后跳转页面,POST请求
  10. .successForwardUrl("/toMain")
  11. //url拦截
  12. http.authorizeRequests()
  13. //login.html不需要被认证
  14. .antMatchers("/login.html").permitAll()
  15. //所有请求都必须被认证,必须登录后被访问
  16. .anyRequest().authenticated();
  17. //关闭csrf防护
  18. http.csrf().disable();
  19. }

controller加@Secured

  1. @Secured(value = "ROLE_acb")
  2. @RequestMapping(value = "/toMain",method = RequestMethod.POST)
  3. public String toMain() {
  4. return "redirect:main.html";
  5. }

3.2、@PreAuthorize/@PostAuthorize

源码:

  1. @Target({ ElementType.METHOD, ElementType.TYPE })
  2. @Retention(RetentionPolicy.RUNTIME)
  3. @Inherited
  4. @Documented
  5. public @interface PreAuthorize {
  6. /**
  7. * @return the Spring-EL expression to be evaluated before invoking the protected
  8. * method
  9. */
  10. String value();
  11. }
  12. @Target({ ElementType.METHOD, ElementType.TYPE })
  13. @Retention(RetentionPolicy.RUNTIME)
  14. @Inherited
  15. @Documented
  16. public @interface PostAuthorize {
  17. /**
  18. * @return the Spring-EL expression to be evaluated after invoking the protected
  19. * method
  20. */
  21. String value();
  22. }
  • @PreAuthorize 表示访问方法或类在执行之前先判断权限,大多情况下都是使用这个注解,注解的参数和access()方法参数取值相同,都是权限表达式。
  • @PostAuthorize 表示方法或类执行结束后判断权限,此注解很少被使用到。

  • @PreAuthorize 在方法调用之前,基于表达式的计算结果来限制对方法的访问

  • @PostAuthorize 允许方法调用,但是如果表达式计算结果为false,将抛出一个安全性异常
  • @PostFilter 允许方法调用,但必须按照表达式来过滤方法的结果
  • @PreFilter 允许方法调用,但必须在进入方法之前过滤输入值

主启动类开启注解

  1. @EnableGlobalMethodSecurity(prePostEnabled = true)

controller新增注解

  1. //@PreAuthorize("hasRole('abc')")
  2. //PreAuthorize的表达式可以以ROLE_开头,也可以不以ROLE_开头,配置类必须ROLE_开头
  3. @PreAuthorize("hasRole('ROLE_abc')")
  • @PreAuthorize里面的参数是access表达式
  • 其中hasRole的值可以为角色名,也可以加ROLE_角色名
  • 配置类里面的hasRole必须为角色名,不可加ROLE
  • access里面hasRole的值可以是角色,也可以加ROLE_角色名

4、记住我

  1. **Spring Security Remember Me 为“记住我”功能,用户只需要在登录时添加 remember-me复选框,取值为trueSpring Security 会自动把用户信息存储到数据源中,以后就可以不登录进行访问**
  1. DEFAULT_REMEMBER_ME_NAME = "remember-me";

4.1、前期准备

导入依赖:

  1. <!-- mybatis 依赖 -->
  2. <dependency>
  3. <groupId>org.mybatis.spring.boot</groupId>
  4. <artifactId>mybatis-spring-boot-starter</artifactId>
  5. <version>2.1.4</version>
  6. </dependency>
  7. <dependency>
  8. <groupId>org.springframework.boot</groupId>
  9. <artifactId>spring-boot-starter-jdbc</artifactId>
  10. </dependency>
  11. <!-- mysql 数据库依赖 -->
  12. <dependency>
  13. <groupId>mysql</groupId>
  14. <artifactId>mysql-connector-java</artifactId>
  15. <version>8.0.18</version>
  16. </dependency>

application.yml

  1. spring:
  2. datasource:
  3. driver-class-name: com.mysql.jdbc.Driver
  4. url: jdbc:mysql://localhost:3306/security?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
  5. username: root
  6. password: root

4.2、修改配置类

注册一个bean

  • PersistentTokenRepository
  1. @Bean
  2. public PersistentTokenRepository persistentTokenRepository(){
  3. JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
  4. jdbcTokenRepository.setDataSource(dataSource);
  5. //自动建表 第二次删除
  6. jdbcTokenRepository.setCreateTableOnStartup(true);
  7. return jdbcTokenRepository;
  8. }

注入元素

  • 顺序不能乱
  1. @Autowired
  2. private DataSource dataSource;
  3. @Autowired
  4. private PersistentTokenRepository persistentTokenRepository;
  5. @Autowired
  6. private UserDetailServiceImpl userDetailService;

参数配置

  1. //记住我
  2. http.rememberMe()
  3. //.rememberMeParameter("remember-me")
  4. //超期时间 默认两周 秒
  5. .tokenValiditySeconds(60)
  6. //自定义登录逻辑
  7. .userDetailsService(userDetailService)
  8. .tokenRepository(persistentTokenRepository);

5、Thymeleaf中SpringSecurity的使用

  1. Spring Security 可以在一些视图技术中进行控制显示效果。例如: JSP Thymeleaf 。在非前后端分离且使用 Spring Boot 的项目中多使用 Thymeleaf 作为视图展示技术。

5.1、前期准备

导入依赖

  1. <!--thymeleaf springsecurity5 依赖-->
  2. <dependency>
  3. <groupId>org.thymeleaf.extras</groupId>
  4. <artifactId>thymeleaf-extras-springsecurity5</artifactId>
  5. </dependency>
  6. <!--thymeleaf依赖-->
  7. <dependency>
  8. <groupId>org.springframework.boot</groupId>
  9. <artifactId>spring-boot-starter-thymeleaf</artifactId>
  10. </dependency>

获取属性

  1. <html xmlns="http://www.w3.org/1999/xhtml"
  2. xmlns:th="http://www.thymeleaf.org"
  3. xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">

获取属性

根据源码得出下面属性:

  • name :登录账号名称
  • principal :登录主体,在自定义登录逻辑中是 UserDetails credentials :
  • 凭证 authorities :权限和角色
  • details :实际上是 WebAuthenticationDetails 的实例。
  • 可以获取 remoteAddress (客户端 ip)和 sessionId (当前 sessionId)

demo.html

  1. <!DOCTYPE html>
  2. <html xmlns="http://www.w3.org/1999/xhtml"
  3. xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
  4. <head>
  5. <meta charset="UTF-8">
  6. <title>demo</title>
  7. </head>
  8. <body>
  9. 登录账号:<span sec:authentication="name"></span><br/>
  10. 登录账号:<span sec:authentication="principal.username"></span><br/>
  11. 凭证:<span sec:authentication="credentials"></span><br/>
  12. 权限和角色:<span sec:authentication="authorities"></span><br/>
  13. 客户端地址:<span sec:authentication="details.remoteAddress"></span><br/>
  14. sessionId:<span sec:authentication="details.sessionId"></span><br/>
  15. </body>
  16. </html>

controller增加跳转

  1. @RequestMapping("/demo")
  2. //@ResponseBody
  3. public String demo(){
  4. return "demo";
  5. }

5.2、权限判断

设置用户角色和权限

  1. AuthorityUtils.commaSeparatedStringToAuthorityList("admin,ROLE_abc,/insert,/delete"));

控制页面显示效果

  1. 通过权限判断:
  2. <button sec:authorize="hasAuthority('/insert')">新增</button>
  3. <button sec:authorize="hasAuthority('/delete')">删除</button>
  4. <button sec:authorize="hasAuthority('/update')">修改</button>
  5. <button sec:authorize="hasAuthority('/select')">查看</button>
  6. <br/>
  7. 通过角色判断:
  8. <button sec:authorize="hasRole('abc')">新增</button>
  9. <button sec:authorize="hasRole('abc')">删除</button>
  10. <button sec:authorize="hasRole('abc')">修改</button>
  11. <button sec:authorize="hasRole('abc')">查看</button>

5.3、注销功能:

  1. <!--<a href="/user/logout">注销</a>-->
  2. <a href="/user/logout">注销</a>
  3. <!--初始-->
  4. <a href="/logout">注销</a>
  1. //注销
  2. http
  3. .logout()
  4. //可以自定义路径
  5. //.logoutUrl("/user/logout")
  6. .logoutSuccessUrl("/login.html");
  7. //销毁HttpSession对象
  8. .invalidateHttpSession()
  9. //清除对象认证状态
  10. .clearAuthentication()
  11. //退出成功处理器
  12. .logoutSuccessHandler()

源码:

  • LogoutConfigurer
  • SecurityContextLogoutHandler
  • LogoutSuccessHandler
  1. //清除session
  2. private boolean invalidateHttpSession = true;
  3. //清除认证对象
  4. private boolean clearAuthentication = true;

作用:

  1. 页面跳转到/logout
  2. 销毁HttpSession
  3. 清除对象的认证状态

5.4、csrf:

  1. 在配置类中一直存在这样一行代码:`http.csrf().disable();`如果没有这行代码导致用户无法被认证。这行代码的含义是:关闭 csrf 防护

什么是csrf

  1. CSRFCross-site request forgery)跨站请求伪造,也被称为“OneClick Attack”或者Session Riding。通过伪造用户请求访问受信任站点的非法请求访问。
  2. 跨域:只要网络协议,ip地址,端口中任何一个不相同就是跨域请求。
  3. 客户端与服务进行交互时,由于http协议本身是无状态协议,所以引入了cookie进行记录客户端身份。在cookie中会存放session id用来识别客户端身份的。在跨域的情况下,session id可能被第三方恶意劫持,通过这个session id向服务端发起请求时,服务端会认为这个请求是合法的,可能发生很多意想不到的事情。

Spring Security中的CSRF
从Spring Security4开始CSRF防护默认开启。默认会拦截请求。进行CSRF处理。CSRF为了保证不是其他第三方网站访问,要求访问时携带参数名为_csrf值为token(token在服务端产生)的内容,如果token和服务端token
匹配成功,则正常访问。

测试:

编写控制器方法
  • 跳转到 templates 中 login.html 页面。
  1. @RequestMapping(value = "/showLogin")
  2. public String showLogin() {
  3. return "login";
  4. }

开启请求伪造
  • 注释即可
  1. //关闭csrf防护关闭 跨站请求伪造
  2. // http.csrf().disable();

login
  1. <!DOCTYPE html>
  2. <html xmlns="http://www.w3.org/1999/xhtml"
  3. xmlns:th="http://www.thymeleaf.org">
  4. <head>
  5. <meta charset="UTF-8">
  6. <title>Title</title>
  7. </head>
  8. <body>
  9. <form action="/login" method="post">
  10. <input type="hidden" name="_csrf" th:value="${_csrf.token}" th:if="${_csrf}">
  11. 用户名:<input type="text" name="username" /><br/>
  12. 密码:<input type="password" name="password" /><br/>
  13. <input type="submit" value="登录" />
  14. </form>
  15. </body>
  16. </html>

6、Oauth2认证

Oauth2简介

  • 第三方认证技术方案最主要是解决认证协议的通用标准问题,因为要实现跨系统认证,各系统之间要 遵循一定的接口协议。
  • OAUTH协议为用户资源的授权提供了一个安全的、开放而又简易的标准。同时,任何第三方都可以 使用OAUTH认证服务,任何服务提供商都可以实现自身的OAUTH认证服务,因而OAUTH是开放的。
  • 业界提供了OAUTH的多种实现如PHP、JavaScript,Java,Ruby等各种语言开发包,大大节约了程序员的 时间,因而OAUTH是简易的。
  • 互联网很多服务如Open API,很多大公司如Google,Yahoo, Microsoft等都提供了OAUTH认证服务,这些都足以说明OAUTH标准逐渐成为开放资源授权的标准。
  • Oauth协议目前发展到2.0版本,1.0版本过于复杂,2.0版本已得到广泛应用。

参考:https://baike.baidu.com/item/oAuth/7153134?fr=aladdin

Oauth 协议:https://tools.ietf.org/html/rfc6749

6.1、实例分析:

SpringSecurity - 图5)

  1. 点击“微信”出现一个二维码,此时用户扫描二维码,开始给网站授权,用户是自己在微信信息的资源拥有者
  2. 资源拥有者同意给客户端授权
    1. 资源拥有者扫描二维码表示资源拥有者同意给客户端授权,微信会对资源拥有者的身份进行验证,
    2. 验 证通过后,微信会询问用户是否给授权网站访问自己的微信数据,用户点击“确认登录”表示同意授权, 微信认证服务器会颁发一个授权码,并重定向到网站。
  3. 客户端获取到授权码,请求认证服务器申请令牌
    • 此过程用户看不到,客户端应用程序请求认证服务器,请求携带授权码。
  4. 认证服务器向客户端响应令牌 —不可见
    • 认证服务器验证了客户端请求的授权码,如果合法则给客户端颁发令牌,令牌是客户端访问资源的通行证。此交互过程用户看不到,当客户端拿到令牌后,用户在网站看到已经登录成功。
  5. 客户端请求资源服务器的资源
    • 客户端携带令牌访问资源服务器的资源。
    • 网站携带令牌请求访问微信服务器获取用户的基本信息。
  6. 资源服务器返回受保护资源
    • 资源服务器校验令牌的合法性,如果合法则向用户响应资源信息内容。

注意:资源服务器和认证服务器可以是一个服务也可以分开的服务,如果是分开的服务资源服务器通常 要请求认证服务器来校验令牌的合法性。

6.2、Oauth2.0认证流程

引自Oauth2.0协议rfc6749 https://tools.ietf.org/html/rfc6749

SpringSecurity - 图6

6.2.1、角色

客户端
  • 本身不存储资源,需要通过资源拥有者的授权去请求资源服务器的资源,
  • 比如:Android客户端、Web 客户端(浏览器端)、微信客户端等。

资源拥有者
  • 通常为用户,也可以是应用程序,即该资源的拥有者。

授权服务器
  • (也称认证服务器)用来对资源拥有的身份进行认证、对访问资源进行授权。
  • 客户端要想访问资源需要通过认证服务器由资 源拥有者授权后方可访问。

资源服务器
  • 存储资源的服务器。
  • 比如,网站用户管理服务器存储了网站用户信息,网站相册服务器存储了用户的相 册信息,微信的资源服务存储了微信的用户信息等。
  • 客户端最终访问资源服务器获取资源信息。

6.2.2、常用术语

1、客户凭证(client Credentials) :客户端的clientId和密码用于认证客户

2、令牌(tokens) :授权服务器在接收到客户请求后,颁发的访问令牌

3、作用域(scopes) :客户请求访问令牌时,由资源拥有者额外指定的细分权限(permission)

6.2.3、优缺点:

优点:
  • 更安全,客户端不接触用户密码,服务器端更易集中保护
  • 广泛传播并被持续采用
  • 短寿命和封装的token
  • 资源服务器和授权服务器解耦
  • 集中式授权,简化客户端
  • HTTP/JSON友好,易于请求和传递token
  • 考虑多种客户端架构场景
  • 客户可以具有不同的信任级别

缺点:
  • 协议框架太宽泛,造成各种实现的兼容性和互操作性差
  • 不是一个认证协议,本身并不能告诉你任何用户信息。

6.3、授权模式(常用)

OAuth2 四种授权模型OAuth 2.0 的四种方式理解OAuth2

1、授权码模式(Authorization Code)

SpringSecurity - 图7

USER-AGENT:浏览器

2、简化授权模式(Implicit)

SpringSecurity - 图8

  • 提前获取访问令牌,但因为在Fragment中无法访问;
  • 通过脚本命令生成

3、密码模式(Resource Owner PasswordCredentials)

SpringSecurity - 图9

4、客户端模式(Client Credentials)

SpringSecurity - 图10

5、刷新令牌

SpringSecurity - 图11

访问令牌过期后不用再重新走一遍流程,可通过刷新令牌从授权服务器重新获取访问令牌

6.4、Spring Security Oauth2

6.4.1、授权服务器

SpringSecurity - 图12

  • Authorize Endpoint :授权端点,进行授权
  • Token Endpoint :令牌端点,经过授权拿到对应的Token
  • Introspection Endpoint :校验端点,校验Token的合法性
  • Revocation Endpoint :撤销端点,撤销授权

6.4.2、Spring Security Oauth2架构

SpringSecurity - 图13

流程:
  1. 用户访问,此时没有Token。Oauth2RestTemplate会报错,这个报错信息会被 Oauth2ClientContextFilter捕获并重定向到认证服务器
  2. 认证服务器通过Authorization Endpoint进行授权,并通过AuthorizationServerTokenServices生成授权码并返回给客户端
  3. 客户端拿到授权码去认证服务器通过Token Endpoint调用AuthorizationServerTokenServices生 成Token并返回给客户端
  4. 客户端拿到Token去资源服务器访问资源,一般会通过Oauth2AuthenticationManager调用 ResourceServerTokenServices进行校验。校验通过可以获取资源。

6.4.3、Spring Security Oauth2

前期准备

创建springboot项目,导入依赖
  1. <properties>
  2. <java.version>1.8</java.version>
  3. <spring-cloud.version>Greenwich.SR2</spring-cloud.version>
  4. </properties>
  5. <dependencies>
  6. <dependency>
  7. <groupId>org.springframework.cloud</groupId>
  8. <artifactId>spring-cloud-starter-oauth2</artifactId>
  9. </dependency>
  10. <dependency>
  11. <groupId>org.springframework.cloud</groupId>
  12. <artifactId>spring-cloud-starter-security</artifactId>
  13. </dependency>
  14. <dependency>
  15. <groupId>org.springframework.boot</groupId>
  16. <artifactId>spring-boot-starter-web</artifactId>
  17. </dependency>
  18. <dependency>
  19. <groupId>org.springframework.boot</groupId>
  20. <artifactId>spring-boot-starter-test</artifactId>
  21. <scope>test</scope>
  22. </dependency>
  23. </dependencies>
  24. <dependencyManagement>
  25. <dependencies>
  26. <dependency>
  27. <groupId>org.springframework.cloud</groupId>
  28. <artifactId>spring-cloud-dependencies</artifactId>
  29. <version>${spring-cloud.version}</version>
  30. <type>pom</type>
  31. <scope>import</scope>
  32. </dependency>
  33. </dependencies>
  34. </dependencyManagement>

实体类

用户-实现UserDetails

  1. public class User implements UserDetails {
  2. private String username;
  3. private String password;
  4. /**
  5. * 权限
  6. */
  7. private List<GrantedAuthority> authorities;
  8. public User(String username, String password, List<GrantedAuthority> authorities) {
  9. this.username = username;
  10. this.password = password;
  11. this.authorities = authorities;
  12. }
  13. @Override
  14. public Collection<? extends GrantedAuthority> getAuthorities() {
  15. return null;
  16. }
  17. //帐户未过期
  18. @Override
  19. public boolean isAccountNonExpired() {
  20. return true;
  21. }
  22. //账户未锁定
  23. @Override
  24. public boolean isAccountNonLocked() {
  25. return true;
  26. }
  27. //凭证未过期
  28. @Override
  29. public boolean isCredentialsNonExpired() {
  30. return true;
  31. }
  32. //是否启用
  33. @Override
  34. public boolean isEnabled() {
  35. return true;
  36. }
  37. @Override
  38. public String getPassword() {
  39. return null;
  40. }
  41. @Override
  42. public String getUsername() {
  43. return username;
  44. }
  45. }

业务逻辑

用户详情服务的定义实现

  1. @Service
  2. public class UserService implements UserDetailsService {
  3. @Autowired
  4. private PasswordEncoder passwordEncoder;
  5. @Override
  6. public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
  7. String password = passwordEncoder.encode("root");
  8. return new User("admin",password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
  9. }
  10. }

配置

安全配置类
  1. @Configuration
  2. //启用Security,启用网络安全
  3. @EnableWebSecurity
  4. public class SecurityConfig extends WebSecurityConfigurerAdapter {
  5. /**
  6. * 密码解析器
  7. *
  8. * @return
  9. */
  10. @Bean
  11. public PasswordEncoder passwordEncoder() {
  12. return new BCryptPasswordEncoder();
  13. }
  14. @Override
  15. protected void configure(HttpSecurity http) throws Exception {
  16. //关闭csrf防护
  17. http.csrf().disable()
  18. //URL权限配置
  19. .authorizeRequests()
  20. //匹配内容
  21. .antMatchers("/oauth/**","/login/**","logout/**")
  22. //允许所有
  23. .permitAll()
  24. //任何请求,必须放在最后,有严格的执行顺序
  25. .anyRequest()
  26. //表示所匹配的 URL 都需要被认证才能访问
  27. .authenticated()
  28. //可以通过and连接,也可以分开写
  29. .and()
  30. //基于表单提交登录
  31. .formLogin()
  32. .permitAll();
  33. }
  34. }

AuthorizationServiceConfig授权服务器
  1. @Configuration
  2. @EnableAuthorizationServer
  3. public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
  4. @Autowired
  5. private PasswordEncoder passwordEncoder;
  6. @Override
  7. public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
  8. ///内存
  9. clients.inMemory()
  10. //配置client-id
  11. .withClient("admin")
  12. //配置client-secret 密钥
  13. .secret(passwordEncoder.encode("112233"))
  14. //配置token有效期
  15. .accessTokenValiditySeconds(3600)
  16. //配置刷新token的有效期
  17. .refreshTokenValiditySeconds(864000)
  18. //配置redirectUris,用于授权成功后跳转
  19. .redirectUris("http://www.baidu.com")
  20. //配置申请的权限的范围
  21. .scopes("all")
  22. //配置grant_type,表示授权类型
  23. .authorizedGrantTypes("authorization_code");
  24. }
  25. }

ResourceServceConfig资源服务器
  1. @Configuration
  2. @EnableResourceServer
  3. public class ResourceServceConfig extends ResourceServerConfigurerAdapter {
  4. @Override
  5. public void configure(HttpSecurity http) throws Exception {
  6. //所有的请求都要经过验证,资源过滤放行"/user下所有的资源"
  7. http.authorizeRequests()
  8. .anyRequest()
  9. .authenticated()
  10. .and()
  11. .requestMatchers()
  12. .antMatchers("/user/**");
  13. }
  14. }

controller

  1. @RestController
  2. @RequestMapping("/user")
  3. public class UserController {
  4. /**
  5. * 获取当前用户
  6. * @param authentication
  7. * @return
  8. */
  9. @RequestMapping("/getCurrentUser")
  10. public Object getCurrentUser(Authentication authentication) {
  11. //返回主体
  12. return authentication.getPrincipal();
  13. }
  14. }

测试

授权码模式

postman调试

grant_type :授权类型,填写authorization_code,表示授权码模式

code :授权码,就是刚刚获取的授权码,

注意:授权码只使用一次就无效了,需要重新申请。

client_id :客户端标识

redirect_uri :申请授权码时的跳转url,一定和申请授权码时用的redirect_uri一致。

scope :授权范围。

认证失败服务端返回 401 Unauthorized

注意:此时无法请求到令牌,访问服务器会报错

密码模式

7、JWT

7.3、JJWT

  • jwt只是一套标准,无法具体应用,所以有很多实现产品
  • JJWT是一个提供端到端的JWT创建和验证的Java库。
  • 永远免费和开源(Apache License,版本2.0),JJW 很容易使用和理解。
  • 它被设计成一个以建筑为中心的流畅界面,隐藏了它的大部分复杂性。
  • 规范官网:https://jwt.io/

1、生成token

  1. 创建项目
  2. 导入依赖
  1. <!--JWT依赖-->
  2. <dependency>
  3. <groupId>io.jsonwebtoken</groupId>
  4. <artifactId>jjwt</artifactId>
  5. <version>0.9.0</version>
  6. </dependency>
  1. 测试
    1. @Test
    2. void contextLoads() {
    3. //1.创建一个jwtbuilder对象
    4. JwtBuilder builder = Jwts.builder();
    5. //2.声明标识{"jti":"888"}
    6. builder.setId("888");
    7. //3.主体,用户{"sub":"Rose"}
    8. builder.setSubject("Rose");
    9. //4.日期{"ita":"xxxxxx"}
    10. builder.setIssuedAt(new Date());
    11. //5.签名,参数1:算法,参数2:盐
    12. builder.signWith(SignatureAlgorithm.HS256,"xxxx");
    13. //6.获取token
    14. String token = builder.compact();
    15. System.out.println(token);
    16. //7.解密 前两部分 第三部分因为盐 保密
    17. String[] split = token.split("//.");
    18. for (String s : split) {
    19. System.out.println(Base64Codec.BASE64.decodeToString(s));
    20. }
    21. }

2、token验证解析

  1. web应用中这个操作是由服务端进行然后发给客户端,客户端在下次向服务端发送请求时需要携带这个token(这就好像是拿着一张门票一样),那服务端接到这个token 应该解析出token中的信息(例如用户id),根据这些信息查询数据库返回相应的结果。
  • 测试
  1. public void testParseToken() {
  2. String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODg4Iiwic3ViIjoiUm9zZSIsImlhdCI6MTYyNjYwNjc3Nn0.pPDKl5_f5f9RHAfGqzaAiwGwQdpOUYMjiQ9BCzWlUR0";
  3. //解析token得到负载中声明的对象
  4. Claims claims = Jwts.parser()
  5. //密钥
  6. .setSigningKey("xxxx")
  7. .parseClaimsJws(token)
  8. //得到主体
  9. .getBody();
  10. System.out.println("id:" + claims.getId());
  11. System.out.println("Subject:" + claims.getSubject());
  12. System.out.println("IssuedAt:" + claims.getIssuedAt());
  13. }

3、token过期检验

  1. public void testCreateTokenHasExp() {
  2. //当前时间
  3. long now = System.currentTimeMillis();
  4. //过期时间,一分钟
  5. long exp = now + 60 * 1000;
  6. //创建 JwtBuilder 对象
  7. JwtBuilder jwtBuilder = Jwts.builder()
  8. //声明的标识,{"jti":"8888"}
  9. .setId("8888")
  10. //设置主体,用户{"sub":"Rose"}
  11. .setSubject("Rose")
  12. //创建时间{"ita":"xxxx"}
  13. .setIssuedAt(new Date())
  14. //创建签证参数1:算法,参数2:盐
  15. .signWith(SignatureAlgorithm.HS256,"xxxx")
  16. //设置过期时间
  17. .setExpiration(new Date(exp));
  18. //获取jwt生成的token
  19. String token = jwtBuilder.compact();
  20. System.out.println(token);
  21. System.out.println("============");
  22. String[] split = token.split("\\.");
  23. System.out.println(Base64Codec.BASE64.decodeToString(split[0]));
  24. System.out.println(Base64Codec.BASE64.decodeToString(split[1]));
  25. //无法解密签名,因为盐 保密
  26. System.out.println(Base64Codec.BASE64.decodeToString(split[2]));
  27. }
  28. public void testParseTokenHasExp() {
  29. String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODg4Iiwic3ViIjoiUm9zZSIsImlhdCI6MTYyNjYwNzgyOSwiZXhwIjoxNjI2NjA3ODg5fQ.6URP2EbehTS2qx_t0medQUPQxaxBWnUl0VJxuEdlboQ";
  30. //解析token得到负载中声明的对象
  31. Claims claims = Jwts.parser()
  32. //密钥
  33. .setSigningKey("xxxx")
  34. .parseClaimsJws(token)
  35. //得到主体
  36. .getBody();
  37. System.out.println("id:" + claims.getId());
  38. System.out.println("Subject:" + claims.getSubject());
  39. System.out.println("IssuedAt:" + claims.getIssuedAt());
  40. //格式化时间
  41. SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
  42. System.out.println("签发时间" + simpleDateFormat.format(claims.getIssuedAt()));
  43. System.out.println("过期时间" + simpleDateFormat.format(claims.getExpiration()));
  44. System.out.println("当前时间" + simpleDateFormat.format(new Date()));
  45. }

4、自定义claims

  1. public void testCreateTokenByClaims() {
  2. //创建 JwtBuilder 对象
  3. JwtBuilder jwtBuilder = Jwts.builder()
  4. //声明的标识,{"jti":"8888"}
  5. .setId("8888")
  6. //设置主体,用户{"sub":"Rose"}
  7. .setSubject("Rose")
  8. //创建时间{"ita":"xxxx"}
  9. .setIssuedAt(new Date())
  10. //创建签证,参数1:算法,参数2:盐
  11. .signWith(SignatureAlgorithm.HS256,"xxxx")
  12. //自定义申明
  13. .claim("roles","admin")
  14. .claim("logo","xxx.jpg");
  15. //直接传入map
  16. //.addClaims(map);
  17. //获取jwt生成的token
  18. String token = jwtBuilder.compact();
  19. System.out.println(token);
  20. System.out.println("============");
  21. String[] split = token.split("\\.");
  22. System.out.println(Base64Codec.BASE64.decodeToString(split[0]));
  23. System.out.println(Base64Codec.BASE64.decodeToString(split[1]));
  24. //无法解密签名
  25. System.out.println(Base64Codec.BASE64.decodeToString(split[2]));
  26. }
  27. public void testParseTokenByClaims() {
  28. String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODg4Iiwic3ViIjoiUm9zZSIsImlhdCI6MTYyNjYwODc3MCwicm9sZXMiOiJhZG1pbiIsImxvZ28iOiJ4eHguanBnIn0.qZUoGPlEELhhObwMmp9pQ3ojrc_xnNTlkTNiJLXwWrc";
  29. //解析token得到负载中声明的对象
  30. Claims claims = Jwts.parser()
  31. //密钥
  32. .setSigningKey("xxxx")
  33. .parseClaimsJws(token)
  34. //得到主体
  35. .getBody();
  36. System.out.println("id:" + claims.getId());
  37. System.out.println("Subject:" + claims.getSubject());
  38. System.out.println("IssuedAt:" + claims.getIssuedAt());
  39. System.out.println("roles:" + claims.get("roles"));
  40. System.out.println("logo:" + claims.get("logo"));
  41. }