SpringBoot整合SpringSecurity

pom文件
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.2.5.RELEASE</version>
  </parent>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
    <thymeleaf.version>3.0.11.RELEASE</thymeleaf.version>
    <thymeleaf-layout-dialect.version>2.1.1</thymeleaf-layout-dialect.version>
  </properties>

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

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
    </dependency>
    
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <version>1.18.12</version>
    </dependency>

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

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

    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>8.0.16</version>
    </dependency>
    <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>druid</artifactId>
      <version>1.0.9</version>
    </dependency>
实体类准备
@Entity
@Table(name = "admin")
@Data
//需要实现UserDetails接口
public class Admin implements UserDetails {

    @Id
    @GeneratedValue
    @Column(name="admin_id")
    private Integer adminId;
    @Column(name = "admin_name")
    private String adminName;
    @Column(name = "admin_pwd")
    private String adminPwd;
    @Column(name = "admin_gender")
    private String adminGender;
    @Column(name = "admin_img")
    private String adminImg;
    @Column(name = "admin_authority")
    private Integer adminAuthority;

    @JsonIgnore
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        //给予角色权限,这里AuthorityPoint类是实现了GrantedAuthority的,模拟Admin的权限信息,实际中可从权限表中查出
        AuthorityPoint authorityPoint = new AuthorityPoint();
        authorityPoint.setAuthorityId(adminAuthority+"");
        return Arrays.asList(authorityPoint);
    }

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

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

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

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

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

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

}
模拟的权限类
@Data
public class AuthorityPoint implements GrantedAuthority {

    private String authorityId;

    public AuthorityPoint() {
    }

  //表示该类的authorityId属性作为权限名
    @Override
    public String getAuthority() {
        return authorityId;
    }
}
中间层
@Service
//需要继承UserDetailsService
public interface AdminService extends UserDetailsService {

}
@Service
public class AdminServiceImpl implements AdminService {

    private Logger log = LoggerFactory.getLogger(AdminServiceImpl.class);

  //使用Jpa做持久层
    @Autowired
    private AdminReposity adminReposity;

  //返回查询出的实体信息
    @Override
    public UserDetails loadUserByUsername(String name) throws
            UsernameNotFoundException {
        log.warn("===============传入的name:{}=============",name);
        Admin admin = adminReposity.getByAdminName(name);
        log.warn("查询出的admin:{}",admin.getAdminName());
        return admin;
    }
}
@Repository
public interface AdminReposity extends JpaRepository<Admin,Integer>{
  	//这里从Admin表查出信息,实际上也要查出对应的权限信息
    @Query("select a from Admin a where a.adminName = ?1")
    Admin getByAdminName(String name);
}
主要配置类
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private AdminService adminService;

    @Autowired
    private BCryptPasswordEncoder passwordEncoder;

    @Autowired
    private DataSource dataSource; // 数据源

  	//认证失败的处理器
    @Bean
    public MyFalureHandler myFalureHandler(){
        return new MyFalureHandler();
    }
	//退出的处理器
    @Bean
    public MyLogoutHandler myLogoutHandler(){
        return new MyLogoutHandler();
    }

    //注入加密对象
    @Bean
    public BCryptPasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(adminService).passwordEncoder(passwordEncoder);
    }

  //用于设置rememberMe往数据库存储token
    @Bean
    public PersistentTokenRepository persistentTokenRepository() {
        JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
        tokenRepository.setDataSource(dataSource); // 设置数据源
        return tokenRepository;
    }

    /**
     * 开启部分路径配置
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().
                antMatchers("/css/**","/font/**","/img/**","/js/**","/json/**",
                        "/layer/**").permitAll() 
                .anyRequest().authenticated()	//访问其他需要认证
                .and()
                .formLogin()
                .loginPage("/admin")	//设置取代默认登录页
                .loginProcessingUrl("/admin/login")	//当在登录页跳转该url时就触发Security的登录请求
                .defaultSuccessUrl("/admin/index") //设置登录成功页面,不要使用successForwardUrl("/admin/index"),可能会出现405错误,原因不知,巨坑!
//                .failureForwardUrl("/admin")
//                .failureHandler(myFalureHandler())
                .permitAll()
                .and()
                .logout().logoutUrl("/admin/logout").logoutSuccessUrl("/admin")//设置触发注销的url以及注销后跳转的url
                .invalidateHttpSession(true)
                .permitAll()
                .and()
                .rememberMe().tokenValiditySeconds(60*6).tokenRepository//设置rememberMe的保存时长
                (persistentTokenRepository())       //把rememberMe信息存到数据库
                .and()
                .csrf()	//禁用csrf
                .disable();
    }


}
自己设置的登录页面
<body id="login">
<div class="login">
    <h2>管理员登录</h2>		<!-- 注意action属性设为配置文件中的触发登录url -->
    <form class="layui-form" method="post" action="/admin/login">
        <p style="color: red" th:if="${param.error}">名字或密码有误~</p>
        <br>
        <div class="layui-form-item">   <!-- 注意name属性设为username -->
            <input type="text" name="username" lay-verify="required"
                   lay-reqText="管理员名不能为空"
                   placeholder="管理员名" class="layui-input" >
            <i class="layui-icon input-icon"></i>
        </div>
        <div class="layui-form-item">	<!-- 注意name属性设为password -->
            <input type="password" name="password" lay-verify="required"
                   lay-reqText="密码不能为空"
                   placeholder="密码" class="layui-input">
            <i class="layui-icon input-icon"></i>
        </div>
        <div class="layui-form-item">	<!-- 注意name属性设为remember-me -->
            <input type="checkbox" name="remember-me" lay-skin="primary" title="记住密码" checked=""> <a class="back" href="javascript:;"  style="margin-top: 10px">忘记密码</a>
        </div>
        <div class="layui-form-item">
            <button style="width: 100%" class="layui-btn" lay-submit lay-filter="login">立即登录</button>
        </div>
    </form>
</div>

这个页面代码直接复制黏贴肯定是不行的,其实自己简单写一个表单,注意把username和password属性,remember-me等属性写对,action也要和配置文件中的**.loginProcessingUrl("/admin/login")**一样,注意:这个url不需要我们在Controller控制器中准备该url方法,这是Security使用的,包括logout路径也不需要我们自己准备

RememberMe功能准备

在数据库里建一张表

CREATE TABLE `persistent_logins` (
`username` varchar(64) NOT NULL,
`series` varchar(64) NOT NULL,
`token` varchar(64) NOT NULL,
`last_used` timestamp NOT NULL,
PRIMARY KEY (`series`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

这样配合上面的配置文件就可以将token保存在数据库了

application.yml文件中没什么好说的,配置了jpa和druid的一些基本信息,不是重点

测试

当你试图访问任何地址时,会根据配置文件中的 loginPage("/admin") 自动跳转到这个url,因为我们没有任何认证,在配置文件中也没有赋权,当然这个url会访问到我们的Controller控制器的方法

@Controller
@RequestMapping("/admin")
public class AdminController {

    private Logger log = LoggerFactory.getLogger(this.getClass());

    @GetMapping({"","/"})
    public String admin(HttpSession session){
        return "login";
    }
}

就是返回一个login.html页面,当我们默认的登录页,如图:

spring security 自定义controller登入_spring

当你点击登录后,跳转loginProcessingUrl("/admin/login") 这个路径,触发Security的登录,这个路径不是我们controller层的东西

defaultSuccessUrl("/admin/index") 登录成功跳转页面

failureHandler(myFalureHandler()) 登录失败跳转的处理器,myFalureHandler()这个方法在配置文件中,就是返回一个我们自定义的处理器MyFalureHandler ,它的代码如下

public class MyFalureHandler implements AuthenticationFailureHandler {

    /**
     * 认证失败的处理
     * @param request
     * @param response
     * @param e
     * @throws IOException
     * @throws ServletException
     */
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse
            response, AuthenticationException e) throws IOException,
            ServletException {
        System.out.println("============认证失败页面============");
        response.sendRedirect("/admin");
    }
}

在这个方法中做你的失败处理逻辑,比如跳转到登录页面,有一个问题,我在做跳转到登录页面时,在这个方法中存储了一些提示信息,往Session域,Request域,RedirectAttributes中都尝试过存储数据然后在登录页面提示失败信息,但是没有用,这是为什么?有人知道的话,欢迎告诉我这个菜鸡

注意从数据库取出的用户密码是加密后的数据,因为我们认证时使用了加密

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
  auth.userDetailsService(adminService).passwordEncoder(passwordEncoder);
}

你可以在测试代码中使用BCryptPasswordEncoder去加密你的用户密码,再存储到数据库模拟

权限相关

我们在实体类中模拟取出Admin的相关权限,权限可以是多个,这里只是模拟并未从数据库中取

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
    //给予角色权限,这里AuthorityPoint类是实现了GrantedAuthority的,模拟Admin的权限信息,实际中可从权限表中查出
    AuthorityPoint authorityPoint = new AuthorityPoint();
    authorityPoint.setAuthorityId(adminAuthority+"");
    return Arrays.asList(authorityPoint);
}

开启注解

@EnableGlobalMethodSecurity(securedEnabled = true)

在相关的Controller中增加权限控制

@Secured("1")	//需要权限名为1的用户才能访问,否则403
@GetMapping("/userlist")
public String userchart(){
    return "admin/userlist";
}

这样就能控制权限了