学习笔记——Spring Boot(9)

Spring Security权限管理

  学习spring boot学深以后自然要接触spring security权限管理,所谓的spring security,就是我们平时接触到的登录时面临的多用户多账户登录,还有用户登录时的安全问题和权限划分的功能。可以说,spring security在进行登录页设计的时候,提供了很多方便,而且拦截器的功能也包括在里面,直接集成就可以了,对登录页面设计也十分友好。

 

  首先当然是导入spring security的依赖:

<!-- Spring Security 依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

  当然,看过我博客的同学也会发现我很喜欢使用thymeleaf作为前端的模板,而thymeleaf为了与spring security结合,也要导入一个依赖(是为了前端设计才导入的):

<!--内含thymeleaf与security结合的模板支持-->
<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity4</artifactId>
    <version>3.0.2.RELEASE</version>
</dependency>

而在需要spring security安全控制的前端html页面中要加入:

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

仅第三条xmlns是新添加的。

 

 

  第一步由于我们是使用数据库进行存储我们用户的账号密码的,所以我们需要建立数据库连接,至于操作数据库可以参考我另一篇博客,我们第一步自然是要建立实体类user(使用者),而spring security要求除了实体user以为,我们还需要建立一个实体authority(权限),用于关联每个账户的权限,除此之外,还有其他很多配置要做,我们先来建立第一个实体类user:

/* *

* 登陆者实体类
* 内含不同权限不同管理权限管理
* */

@Entity
@Table(name = "user")
//继承UserDetails实现spring security的缓存机制
public class User implements UserDetails ,Serializable {

    //user的序列化id
    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    @NotBlank
    @Column(nullable = false,length = 50)
    private String username;


    @NotBlank
    @Column(nullable = false,length = 50)
    private String password;


    //建立一个与Authority数据表对应的List,其中是多对多的关系
    //其中蕴含了一个中间表user_authority说明两个表的关系
    @ManyToMany(cascade = CascadeType.DETACH,fetch = FetchType.EAGER)
    @JoinTable(name = "user_authority",joinColumns  = @JoinColumn(name = "user_id",referencedColumnName = "id"),inverseJoinColumns = @JoinColumn(name = "authority_id",referencedColumnName = "id"))
    private List<Authority> authorities;


    public Integer getId() {
        return id;
    }

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


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

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

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

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

    public static long getSerialVersionUID() {
        return serialVersionUID;
    }

    public void setAuthorities(List<Authority> authorities) {
        this.authorities = authorities;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        //需要将list<authority>转换为List<SimpleGrantedAuthority>,否则会拿不到角色列表
        List<SimpleGrantedAuthority> simpleGrantedAuthorities=new ArrayList<>();
        for(GrantedAuthority authority:this.authorities){
            simpleGrantedAuthorities.add(new SimpleGrantedAuthority(authority.getAuthority()));
        }
        return simpleGrantedAuthorities;
    }


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

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

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

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

记住观察好我这个实体,其中spring security是要求继承userDetails的,同时也要实现userDetails的各个方法,这是为了更好实现spring security的缓存机制。而Serializable是用于序列化的,这也是必须的。另外,该实体类是与下面的authority权限实体类建立了manytomany的数据库关系的,是为了更好的管理账户的权限。

 

下面是建立authority权限的实体类:

/* *
* 权限类,是与user进行耦合
* */
@Entity
@Table(name = "authority")
public class Authority implements GrantedAuthority {

    //authority的序列化id
    private static final long serialVersionUID = 1L;

    @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;
    }

    @Override
    public String getAuthority() {
        return name;
    }

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

这是权限实体类,十分简单,仅需继承GrantedAuthority,且实现两个参数id和name。

 

 

下面就是建立与数据库的基础连接了,也就是如果要对各个账户进行增删改查,则需要建立repository和service进行管理。第一步是要建立user的repository:

public interface UserRepository extends JpaRepository<User,Integer> {

    //根据用户名查询用户
    User findByUsername(String username);

}

十分简单,但是记住一定要实现一个根据用户名查找用户的方法,这在建立service中userDetailsService中要使用的。

 

而建立authority的repository更简单:

public interface AuthorityRepository extends JpaRepository<Authority,Integer> {

}

 

 

下面就是建立service层了,首先是service层的接口,然后才是接口的实现类,两个接口我就简单发一下:

/* *
* user的service层接口
* */
public interface UserService  {

    List<User> findAll();

    User findById(Integer id);

    User saveOrUpdateUser(User user);

    void deleteById(Integer id);
}
/* *
* authority的service接口
* */
public interface AuthorityService {

    //根据id查询权限
    Authority getAuthorityById(Integer id);

    //查询所有权限
    List<Authority> findAll();
}

 

  下面是user类接口的实现类:

@Service
public class UserServiceImpl implements UserService, UserDetailsService {
    @Autowired
    private UserRepository userRepository;

    @Override
    @Transactional
    public List<User> findAll() {
        return userRepository.findAll();
    }

    @Override
    @Transactional
    public User findById(Integer id) {
        return userRepository.findOne(id);
    }


    @Override
    @Transactional
    public User saveOrUpdateUser(User user) {
        try{
           userRepository.save(user);
        }catch (Exception e){
            throw new RuntimeException("Add User Error: "+e.getMessage());
        }
        return user;
    }

    @Override
    @Transactional
    public void deleteById(Integer id) {
         userRepository.delete(id);
    }

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(s);
        if(user==null){
            throw new UsernameNotFoundException("用户名不存在");
        }//用户不存在要抛出异常
        return user;
    }
}

spring security要求继承userDetailsService,然后要实现一个方法,就是loadUserByUsername,具体实现可以观察上面。

 

  Authority权限接口实现类实现十分简单:

@Service
public class AuthorityServiceImpl implements AuthorityService {

    @Autowired
    private AuthorityRepository authorityRepository;

    @Override
    public Authority getAuthorityById(Integer id) {
        return authorityRepository.findOne(id);
    }

    @Override
    public List<Authority> findAll() {
        return authorityRepository.findAll();
    }
}

 

 

  现在我们对账号管理就差不多做完了(除了controller,由于controller和我之前管理数据库的博客的实现差不多,也就不再展示了),对其的增删改查就需要我们设计前端页面进行。但是下面才是重点,也就是设计登录页时的各类功能,也就是spring security的核心。首先我们需要建立spring security的配置类,通过配置类进行一系列操作:

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)  //启用安全认证
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    //定义一个key
    private static final String KEY = "scnu";

    //注入UserDetailsService
    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private PasswordEncoder passwordEncoder;
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();  //使用BCrypt加密
    }

    //实现方法authenticationProvider(),内含密码加密
    @Bean
    public AuthenticationProvider authenticationProvider(){
        //DaoAuthenticationProvider用于从UserDetailsService中取出认证信息
        DaoAuthenticationProvider daoAuthenticationProvider=new DaoAuthenticationProvider();
        daoAuthenticationProvider.setUserDetailsService(userDetailsService);
        daoAuthenticationProvider.setPasswordEncoder(passwordEncoder); //密码加密
        return daoAuthenticationProvider;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/asserts/**","/login.html").permitAll()  //静态资源可以访问
                .antMatchers("/h2-console/**").permitAll() // h2控制台都可以访问
                .antMatchers("/admin/**").hasRole("ADMIN")  //管理页需要admin角色才可以访问
                .antMatchers("/setter/**").hasRole("USER")
                .and()
                .formLogin()  //基于form表单的访问形式
                .loginPage("/login").defaultSuccessUrl("/dispath").failureUrl("/login-error")  //设置登录页,成功后访问的页面和访问错误页
                .and().rememberMe().key(KEY)  //remember-me的设置
                .and().exceptionHandling().accessDeniedPage("/403");  //账号密码错误进入403界面
        http.csrf().ignoringAntMatchers("/h2-console/**"); // 禁用 H2 控制台的 CSRF 防护
        http.headers().frameOptions().sameOrigin(); // 允许来自同一来源的H2 控制台的请求
    }

    /* *
    * 认证信息从数据库从获取
    * */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService);   //使用数据库存储的信息
        auth.authenticationProvider(authenticationProvider());   //密码加密使用BCrypt加密算法
    }
}

我们需要将该配置类继承WebSecurityConfigurerAdapter,使用注解@EnableWebSecurity声明。该类主要改造了继承的WebSecurityConfigurerAdapter中方法configure(HttpSecurity http)和configure(AuthenticationManagerBuilder auth),第一个方法是实现一些基础的配置,比如哪些页面可以访问,哪些页面需要什么权限才可以访问,成功登陆会访问什么,密码错误会访问什么,remember me等配置,我在该方法中都有注释,可以自行观察,另外,在remember me中需要一个KEY,我们需要在方法外声明,至于KEY的值我们可以自行赋值。第二个方法是配置账户信息通过数据库存储,同时密码加密的形式,我密码加密的形式是使用BCrypt加密算法,也就是说上面有多个bean配置都是为了密码加密而声明的。

 

 

  现在来说一下与登录页相关的controller层的设计,控制层的设计与刚刚配置类中的方法configure(HttpSecurity http)息息相关,我们观察一下刚刚的方法:formLogin()表明是基于form表单的形式进行登录,loginPage(“/login”)指明登录页的url,defaultSuccessful(“/dispath”)指明登录成功访问的url,failureUrl(“/login-error”)指明当账号密码出现错误时访问的url,我们可以根据这些url设计controller:

/* *
* 登录页的控制层设置
* */
@Controller
public class LoginController {

    @GetMapping({"/login","/"})
    public String loginPage(){
        return "login";
    }

    //发生账号密码错误时候
    @GetMapping("/login-error")
    public String errorMsg(Model model){
        model.addAttribute("loginError",true);
        model.addAttribute("errorMsg","账号密码错误!");
        return "login";
    }

    /* *
     * 该方法是根据不同用户权限跳转至不同用户所需界面
     * admin和user两种用户为主
     * */
    @GetMapping("/dispath")
    public String dispath(){
        //获取当前用户的权限
        Set<String> roles = AuthorityUtils.authorityListToSet(SecurityContextHolder.getContext()
                .getAuthentication().getAuthorities());

        //设置变量存储路径地址
        String s="";
        if(roles.contains("ROLE_ADMIN")){
            s="redirect:/admin";
        }else if(roles.contains("ROLE_USER")){
            s="forward:/setter ";
        }        //根据权限跳转
        return s;
    }
}

可以观察方法dispath()中,我们是根据用户的不同权限进行跳转的。

 

 

  顺便给一下登录页前端页面的form表单(基于thymeleaf):

<form class="form-signin" action="#" th:action="@{login}" method="post">
   <h1 class="h3 mb-3 font-weight-normal" >请登录</h1>
   <!--加入p标签,标签的颜色设置为红色,标签的内容由controller中msg获得-->
   <!--使用if方法,同时变量表达式中的内置工具判断msg是否为空-->
   <p style="color: red" th:text="${errorMsg}" th:if="${loginError}==true"></p>
   <label class="sr-only" >用户名</label>
   <input type="text" name="username" class="form-control" placeholder="请输入账号"  required="" autofocus="">
   <label class="sr-only">密码</label>
   <input type="password" name="password" class="form-control" placeholder="请输入密码"  required="">
   <div class="checkbox mb-3">
      <label>
        <input type="checkbox"  name="remember-me" >记住我
      </label>
   </div>
   <button class="btn btn-lg btn-primary btn-block" type="submit" >登录</button>
</form>

由于在配置中我们已经声明了是基于form表单的形式,所以我们仅赋值name与相关的参数即可自动配置完。其中,remember me的功能是在之前配置类中方法configure(HttpSecurity http)中声明rememberMe().key(KEY)中导入,我们只需要在登录页form表单中功能记住我声明name为remember-me即可。

 

 

重点关注:

  由于加入了spring security,所以在进行某些增删改查的时候你会发现之前的方法不行,这是因为加入了跨域防护这种烦人的东西,但是没有又不安全,所以我们需要在前端页面中加入一些参数来增加跨域防护,但其实我自己学得也不太好,所以说得也不太好,首先我们需要在head标签中加入:

<!-- CSRF -->
<meta name="_csrf" th:content="${_csrf.token}"/>
<!-- default header name is X-CSRF-TOKEN -->
<meta name="_csrf_header" th:content="${_csrf.headerName}"/>

这是与csrf跨域防护的声明,然后我们在设计按钮(button或者a标签)的时候,要使用JavaScript时进行按钮设计时,在ajax的设计前我们需要引入:

<!--// 获取 CSRF Token-->
var csrfToken = $("meta[name='_csrf']").attr("content");
var csrfHeader = $("meta[name='_csrf_header']").attr("content");

给个例子,我们首先声明了一个删除按钮:

<!--删除按钮-->
<a class="btn btn-danger btn-sm deletebtn" role="button" data-th-attr="userId=${user.id}">删除</a>

然后进行js(含ajax)设计:

<script>
          $(".deletebtn").click(function () {
              // 获取 CSRF Token
              var csrfToken = $("meta[name='_csrf']").attr("content");
              var csrfHeader = $("meta[name='_csrf_header']").attr("content");
              if(window.confirm("你确定要删除吗?")) {
                  $.ajax({
                      url: "/admin/" + $(this).attr("userId"),
                      type: 'DELETE',
                      beforeSend: function (request) {
                          request.setRequestHeader(csrfHeader, csrfToken); // 添加  CSRF Token
                      },
                      success: function (data) {
                          alert("删除成功!");
                          //成功了刷新界面
                          $("#mainContainer").html(data);
                      }
                   })
                  return true;
              }
          })
</script>

大概就是这样,但是里面还有一些问题,由于我对前端知识的缺漏,所以会有一些问题,希望有大神可以指点一下。

 

另外,如果简单的声明一下为form表单的形式,前端会自动注入跨域防护,比如删除按钮这样子声明的话:

<!--删除按钮-->
<form  th:action="@{/admin/}+${user.id}" method="post">
   <input type="hidden" name="_method" value="delete">
   <!--删除按钮-->
   <button  type="submit" class="btn btn-danger btn-sm deletebtn">删除</button>
</form>

它将会自动生成csrf防护(我也不清楚什么机制)。

 

 

猜你喜欢

转载自blog.csdn.net/nanshenjiang/article/details/82950111