当前位置:   article > 正文

SpringCloud(part11) Spring Security详解_spring-cloud-starter-security

spring-cloud-starter-security

1.Spring Security简介

  • 什么是Spring Security

Spring Security采用安全层的概念,使得controller,Service,dao层等以注解的方式来保护应用程序的安全。Spring Security提供了细粒度的权限控制,可以精细到每一个API接口,每一个业务方法,或者每一个操作数据库的DAO层方法。Spring Security提供的是应用程序层的安全解决方法,一个系统的安全还需要考虑传输层和系统层的安全,例如HTTPS协议,服务器部署防火墙等。

  • 为什么选择Spring Security

  对环境的无依赖性,低代码耦合性。将工程部署到一个服务器上,不需要为Spring Security做什么工作。Spring Security提供了十几个安全模块,模块与模块之间耦合性低,而且模块之前可以自由组合来实现特定的需求的安全功能,具有较高的可自定性。

  在安全方面,认证和授权。

  Apache Shiro一半使用在单体架构中,对于微服务架构是无能为力的,Spring Security适合于微服务架构。

2.案例

  • 依赖:
  1. <dependencies>
  2. <dependency>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-starter-web</artifactId>
  5. </dependency>
  6. <dependency>
  7. <groupId>org.springframework.cloud</groupId>
  8. <artifactId>spring-cloud-starter-security</artifactId>
  9. </dependency>
  10. <dependency>
  11. <groupId>org.springframework.boot</groupId>
  12. <artifactId>spring-boot-starter-thymeleaf</artifactId>
  13. </dependency>
  14. <!--thymeleaf 对 Spring Security的支持-->
  15. <dependency>
  16. <groupId>org.thymeleaf.extras</groupId>
  17. <artifactId>thymeleaf-extras-springsecurity5</artifactId>
  18. <version>3.0.3.RELEASE</version>
  19. </dependency>
  20. <dependency>
  21. <groupId>org.springframework.boot</groupId>
  22. <artifactId>spring-boot-starter-test</artifactId>
  23. <scope>test</scope>
  24. </dependency>
  25. </dependencies>
  26. <dependencyManagement>
  27. <dependencies>
  28. <dependency>
  29. <groupId>org.springframework.cloud</groupId>
  30. <artifactId>spring-cloud-dependencies</artifactId>
  31. <version>${spring-cloud.version}</version>
  32. <type>pom</type>
  33. <scope>import</scope>
  34. </dependency>
  35. </dependencies>
  36. </dependencyManagement>
  • 配置:
  1. server:
  2. port: 8001
  3. spring:
  4. thymeleaf:
  5. cache: false
  6. mode: LEGACYHTML5
  7. suffix: .html
  • 配置WebSecurityConfigurerAdapter
  1. @EnableWebSecurity
  2. @Configuration
  3. public class SecurityConfig extends WebSecurityConfigurerAdapter {
  4. //通过这个类在AuthenticationManagerBuilder内存中
  5. //创建了一个认证用户的信息
  6. /*
  7. 代码较少但是做了很多安全防护的功能:
  8. 1.应用层的每一个请求都不需要认证
  9. 2.自动生成了一个登陆表单
  10. 3.可以通过name和password来进行认证
  11. 4.用户可以注销
  12. 5.防止了CSRF攻击
  13. 6.Session Fixation保护
  14. 7.安全Header集成...
  15. */
  16. @Override
  17. protected void configure(AuthenticationManagerBuilder auth) throws Exception {
  18. auth.inMemoryAuthentication().
  19. withUser("zht").password("123456").roles("admin");
  20. }
  21. }

注意:如果你使用的是Security5,需要对密码进行加密,不然登录时会抛出运行时异常

  1. @Autowired
  2. protected void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
  3. //inMemoryAuthentication 从内存中获取 对内存中的密码进行Bcrypt编码加密
  4. auth.inMemoryAuthentication().
  5. passwordEncoder(new BCryptPasswordEncoder()).
  6. withUser("zht").
  7. password(
  8. new BCryptPasswordEncoder().
  9. encode("123456")).
  10. roles("USER");
  11. auth.inMemoryAuthentication().
  12. passwordEncoder(new BCryptPasswordEncoder()).
  13. withUser("admin").
  14. password(
  15. new BCryptPasswordEncoder().
  16. encode("123456")).
  17. roles("admin");
  18. }
  • 配置HttpSecurity

WebSecurityConfiguraterAdappter已经配置了如何验证用户信息,那么SpringSecurity是否知道所用的用户都需要身份验证,又如何知道要支持基于表单的身份验证呢?工程中的那些资源需要验证?那些资源不需要验证?这时就需要配置HttpSecurity。

在继承 WebSecurityConfigurerAdapter 的类中 重写 configure(HttpSecurity http)方法来配置

  1. @Override
  2. protected void configure(HttpSecurity http) throws Exception {
  3. http
  4. .authorizeRequests()
  5. .antMatchers("/css/**","/index").permitAll()
  6. .antMatchers("/user/**").hasRole("admin")
  7. .antMatchers("/blogs/**").hasRole("admin")
  8. .and()
  9. .formLogin().loginPage("/login").failureForwardUrl("/login-error")
  10. .and()
  11. .exceptionHandling().accessDeniedPage("/401");
  12. http.logout().logoutSuccessUrl("/");
  13. }
  14. /*
  15. 1.以 /css/**开头的资源和 /index 资源不需要验证
  16. 2.以 /user/** 和 、/blogs/** 开头的资源需要验证,而且需要用户的角色是admin
  17. 3.表单登陆的地址是/login,登陆失败的地址是/login-error
  18. 4.异常处理会重定向 /401 界面
  19. 5.注销登录成功,成功重定向到首页
  20. */

首页:

注意这里有一个版本的不兼容问题,boot2.1.5springsecurity4不匹配

  1. <!DOCTYPE html>
  2. <html xmlns="http://www.w3.org/1999/xhtml"
  3. xmlns:th="http://www.thymeleaf.org"
  4. xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
  5. <head>
  6. <title>Good Thymes Virtual Grocery</title>
  7. <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
  8. </head>
  9. <body>
  10. <h1>Hello Spring Security</h1>
  11. <p>这个页面没有受到保护,你可以进行受保护的页面</p>
  12. <div th:fragment="logout" sec:authorize="isAuthenticated()">
  13. 登录用户:<span sec:authentication="name"></span>|
  14. 用户角色:<span sec:authentication="principal.authorities"></span>
  15. <div>
  16. <form action="#" th:action="@{/logout}" method="post">
  17. <input type="submit" value="注销"/>
  18. </form>
  19. </div>
  20. </div>
  21. <ul><li>
  22. <a href="/user/index" th:href="@{/user/index}">去/user/index被保护的页面</a>
  23. </li></ul>
  24. </body>
  25. </html>

登录页面

  1. <!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-4.dtd">
  2. <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
  3. <head>
  4. <title>Good Thymes Virtual Grocery</title>
  5. <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
  6. </head>
  7. <body>
  8. <p>User 角色 zht / 123456</p>
  9. <p th:if="${loginError}" style="color: red">用户名密码错误</p>
  10. <form th:action="@{/login}" method="post">
  11. <label for="username">用户名</label>
  12. <input id="username" name="username" type="text"/>
  13. <label for="password">密码</label>
  14. <input id="password" name="password" type="password"/>
  15. <input type="submit" value="登录"/>
  16. </form>
  17. <a th:href="@{/index}">返回主页</a>
  18. </body>
  19. </html>

权限不足,401页面

  1. <!DOCTYPE html>
  2. <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
  3. xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
  4. <head>
  5. <title>Good Thymes Virtual Grocery</title>
  6. <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
  7. </head>
  8. <body>
  9. <h1>权限不够,拒绝访问</h1>
  10. <div sec:authorize="isAuthenticated()">
  11. <p>用户:<span sec:authentication="name"></span></p>
  12. <p>角色:<span sec:authentication="principal.authorities"></span></p>
  13. </div>
  14. <div sec:authorize="isAuthenticated()">
  15. <p>未有用户登录</p>
  16. </div>
  17. </body>
  18. </html>

受保护的首页:

  1. <!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-4.dtd">
  2. <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
  3. <head>
  4. <title>Good Thymes Virtual Grocery</title>
  5. <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
  6. </head>
  7. <body>
  8. <div th:substituteby="index::logout"></div>
  9. <h1>这个是Security保护的界面</h1>
  10. <p><a href="/index" th:href="@{/index}">返回首页</a></p>
  11. <p><a href="/blogs" th:href="@{/blogs}">管理博客</a></p>
  12. </body>
  13. </html>

3.Spring Security方法级别上的保护

Spring Securiyt2.0版本开始,提供了方法级别的安全支持,并提供了JSR-250的支持。

在以上Demo的基础之上:

定义Service接口

public interface BlogService {
    List<Blog> getBlogs();
    void deleteBlog(Integer id);
}

实现类:

@Service
public class BlogServiceImp implements BlogService {

    private List<Blog> blogs=new ArrayList<Blog>();

    public BlogServiceImp() {
      blogs.add(new Blog(1,"Spring Security方法级别上的保护","good!"));
      blogs.add(new Blog(2,"微服务如何独立部署","每个服务单独部署会不会很麻烦!" +
              "如果通过传统的方式很麻烦,我们只需要通过Docker"));
    }

    @Override
    public List<Blog> getBlogs() {
        return blogs;
    }

    @Override
    public void deleteBlog(Integer id) {
        Iterator iterator=blogs.iterator();
        while(iterator.hasNext()){
            Blog blog= (Blog) iterator.next();
            if(blog.getId()==id){
               iterator.remove();//注意这个地方
            }
        }
    }
}

在控制器方法上加入权限

@RestController
@RequestMapping("/blogs")
public class controller {
    @Autowired
    BlogService blogService;

    @GetMapping
    public ModelAndView list(Model model){
        List<Blog> blogs=blogService.getBlogs();
        model.addAttribute("blogsList",blogs);
        return new ModelAndView("blog/list","blogModel",model);
    }
    @PreAuthorize("hasAuthority('ROLE_admin')")
    //在方法上开启权限注解
    @GetMapping("/{id}/deletion")
    public ModelAndView delete(Model model,@PathVariable("id") Integer id){
        System.out.println("controller。。。");
        blogService.deleteBlog(id);
        model.addAttribute("blogsList",blogService.getBlogs());
        return new ModelAndView("blog/list","blogModel",model);
    }

}

配置类:

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)//开启方法级别的注解
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    /*
        @EnableGlobalMethodSecurity
        可选参数:prePostEnabled的Pre和Post注解是否可用。即@PreAuthorize和@PostAuthorize
                  secureEnabled: Spring Security的@Secured 注解是否可用
                  jsr250Enabled: Spring Security对JSR-250的注解是否可用
        一般我们只会用到@PreAuthorize注解会在进入方法前进行权限验证
     */
    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    protected void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        
        inMemoryAuthentication //从内存中获取  对内存中的密码进行Bcrypt编码加密
        auth.inMemoryAuthentication().
                passwordEncoder(new BCryptPasswordEncoder()).
                withUser("zht").
                password(
                        new BCryptPasswordEncoder().
                                encode("123456")).
                roles("USER");
        auth.inMemoryAuthentication().
                passwordEncoder(new BCryptPasswordEncoder()).
                withUser("admin").
                password(
                        new BCryptPasswordEncoder().
                                encode("123456")).
                roles("admin","USER");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers("/css/**","/index").permitAll()
                .antMatchers("/user/**").hasRole("USER")
                .antMatchers("/blogs/**").hasRole("USER")
                .and()
                .formLogin().loginPage("/login").failureForwardUrl("/login-error")
                .and()
                .exceptionHandling().accessDeniedPage("/401");
        http.logout().logoutSuccessUrl("/");
        http.formLogin().successForwardUrl("/index");
    }
}

增加页面:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<head>
    <title>Good Thymes Virtual Grocery</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
</head>
<body>
    <table>
        <tr>
            <td>博客编号</td>
            <td>博客名称</td>
            <td>博客内容</td>
            <td>操作</td>
        </tr>
        <tr th:each="blog:${blogsList}">
            <td th:text="${blog.id}"></td>
            <td th:text="${blog.name}"></td>
            <td th:text="${blog.content}"></td>
            <td><a th:href="@{'/blogs/'+${blog.id}+'/deletion'}">删除</a></td>
        </tr>
    </table>
</body>
</html> 

4.通过操作数据库 ORM (JPA)访问权限

  • 配置
server:
  port: 8002
spring:
  thymeleaf:
    cache: false
    mode: LEGACYHTML5
    suffix: .html
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/spring-security
    username: root
    password: 123456
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true
  • 建立实体类
package com.example.springbootsecurity2.entity;

import java.io.Serializable;

public class Blog implements Serializable {
    private Integer id;
    private String name;
    private String content;

    public Blog(Integer id, String name, String content) {
        this.id = id;
        this.name = name;
        this.content = content;
    }
    public Blog() {
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }
}
package com.example.springbootsecurity2.entity;

import org.springframework.security.core.GrantedAuthority;

import javax.persistence.*;

@Entity
public class Role implements GrantedAuthority {
    //GrantedAuthority 权限接口
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    @Column(nullable = false)
    private String name;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String getAuthority() {
        return name;//返回name作为权限认证的标志
    }

    @Override
    public String toString() {
        return "Role{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}
package com.example.springbootsecurity2.entity;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import javax.persistence.*;
import java.io.Serializable;
import java.util.Collection;
import java.util.List;

@Entity
public class User implements UserDetails, Serializable {
    //实现UserDetails接口,该接口是实现Spring Security认证信息的核心接口。
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)//主键自增
    private Integer id;

    @Column(nullable = false,unique = true)
    private String username;
    @Column
    private String password;

    @ManyToMany(cascade = CascadeType.ALL,fetch = FetchType.EAGER)
    @JoinTable(name = "user_role",joinColumns =
    @JoinColumn(name = "user_id",referencedColumnName = "id"),
            inverseJoinColumns =@JoinColumn(name = "role_id",referencedColumnName = "id") )
    private List<Role> authorities;



    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

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

    @Override
    public String getUsername() {
        return username;
        //这个方法不用就要返回username。也可以是手机号、邮箱,qq号等
    }

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

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

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

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

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public void setAuthorities(List<Role> authorities) {
        this.authorities = authorities;
    }
}
  • dao层操作接口
public interface UserDAO extends JpaRepository<User,Integer> {
    User findByUsername(String name);
}
  • 修改配置类,从数据库读取
Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)//开启方法级别的注解
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    /*
        @EnableGlobalMethodSecurity
        可选参数:prePostEnabled的Pre和Post注解是否可用。即@PreAuthorize和@PostAuthorize
                  secureEnabled: Spring Security的@Secured 注解是否可用
                  jsr250Enabled: Spring Security对JSR-250的注解是否可用
        一般我们只会用到@PreAuthorize注解会在进入方法前进行权限验证
     */
    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    protected void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        /*
        inMemoryAuthentication 从内存中获取  对内存中的密码进行Bcrypt编码加密
        auth.inMemoryAuthentication().
                passwordEncoder(new BCryptPasswordEncoder()).
                withUser("zht").
                password(
                        new BCryptPasswordEncoder().
                                encode("123456")).
                roles("USER");
        auth.inMemoryAuthentication().
                passwordEncoder(new BCryptPasswordEncoder()).
                withUser("admin").
                password(
                        new BCryptPasswordEncoder().
                                encode("123456")).
                roles("admin","USER");
                */
        //更换从数据库获取权限信息 Security 5 以后必须对密码进行加密
        auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers("/css/**","/index").permitAll()
                .antMatchers("/user/**").hasRole("USER")
                .antMatchers("/blogs/**").hasRole("USER")
                .and()
                .formLogin().loginPage("/login").failureForwardUrl("/login-error")
                .and()
                .exceptionHandling().accessDeniedPage("/401");
        http.logout().logoutSuccessUrl("/");
        http.formLogin().successForwardUrl("/index");
    }
}
  • 控制器接口
@Controller
public class Testcontroller {
    @RequestMapping("/")
    public String root(){
        return "redirect:index";
    }
    @RequestMapping("/index")
    public String index(){
        return "index";
    }
    @RequestMapping("/login")
    public String login(){
        return "login";
    }
    @RequestMapping("/login-error")
    public String loginError(Model model){
        model.addAttribute("loginError",true);
        return "login";
    }
    @RequestMapping("/401")
    public  String acccessDenied(){
        return "401";
    }

    @RequestMapping("/user/index")
    public String toUserIndex(){
        return "/user/index";
    }
}
@RestController
@RequestMapping("/blogs")
public class controller {
    @Autowired
    BlogService blogService;

    @GetMapping
    public ModelAndView list(Model model){
        List<Blog> blogs=blogService.getBlogs();
        model.addAttribute("blogsList",blogs);
        return new ModelAndView("blog/list","blogModel",model);
    }
    @PreAuthorize("hasAuthority('ROLE_admin')")
    //在方法上开启权限注解
    @GetMapping("/{id}/deletion")
    public ModelAndView delete(Model model,@PathVariable("id") Integer id){
        System.out.println("controller。。。");
        blogService.deleteBlog(id);
        model.addAttribute("blogsList",blogService.getBlogs());
        return new ModelAndView("blog/list","blogModel",model);
    }

}

还要往数据库中加入记录。

1    $2a$10$6V/KuYL7RyIELEcs4SEmkOuI1I5tF3jo0xOuZIxuHRu/LfxJv6i5q    zht
2    $2a$10$6V/KuYL7RyIELEcs4SEmkOuI1I5tF3jo0xOuZIxuHRu/LfxJv6i5q    admin

1    ROLE_USER
2    ROLE_admin

 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/很楠不爱3/article/detail/185331
推荐阅读
相关标签
  

闽ICP备14008679号