Spring Boot+Spring Security+Spirng Data Jpa实现登录权限验证并实现自动登录

Spring Boot+Spring Security+Spirng Data Jpa实现登录权限验证并实现自动登录

Scroll Down

实现功能:本案例中有三个用户,他们的角色分别为管理员、老师、学生。管理员可以访问任意页面,而老师和学生只能访问自己的页面。

环境搭建

导入坐标

当前springboot版本为2.4.1

<!--spring data jpa-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!--spring security-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--thymeleaf模板-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!--thymeleaf中使用的spring security标签-->
<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity5</artifactId>
    <version>3.0.3.RELEASE</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.5.7</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-test</artifactId>
    <scope>test</scope>
</dependency>

Spring Data Jpa相关配置

实体类编写

Users.java : 用户表

@Setter
@Getter
@Entity
@Table(name = "users")
public class Users {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    private String username;
    private String password;

    @ManyToMany(targetEntity = Authorities.class, cascade = CascadeType.ALL)
    @JoinTable(name = "users_authorities",
            joinColumns = @JoinColumn(name = "users_id", referencedColumnName = "id"),
            inverseJoinColumns = @JoinColumn(name = "authorities_id", referencedColumnName = "id"))
    private Set<Authorities> authorities = new HashSet<>();
}

为什么不使用lombok中的@Data注解呢?

@Data重写的toString()方法会包括所有属性,在打印控制台的时候会出现循环引用导致栈溢出错误。自己重写的toString()尽量不要包含有关外键、中间表的属性。

Authorities.java : 角色表,用于存储角色信息,与用户表是多对多关系

@Setter
@Getter
@Entity
@Table(name = "authorities")
public class Authorities{
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    private String authority;

    @ManyToMany(mappedBy = "authorities", cascade = CascadeType.ALL)
    private Set<Users> users = new HashSet<>();
}

PersistentLogins.java : 用于自动登录功能中把生成的token信息存储数据库中(可选)

/*
 * 仅用于自动登录(记住密码)建表,spring security提供的建表语句第二次启动会报错
 * */
@Entity
@Table(name = "persistent_logins")
public class PersistentLogins {
    @Id
    private String series;
    private String username;
    private String token;
    private Date last_used;
}

dao接口层

public interface UsersDao extends JpaRepository<Users, Integer>, JpaSpecificationExecutor<Users> {
    Users findByUsername(String username);
}
public interface AuthoritiesDao extends JpaRepository<Authorities, Integer>, JpaSpecificationExecutor<Authorities> {
}

Spring security相关配置

实现UserDetailsService接口

用于访问数据库中的信息,之后要把它配置在spirng security中

@Service("userDetailsService")
@Slf4j
public class MyUserDetailService implements UserDetailsService {
    @Autowired
    private UsersDao usersDao;

    @Override
    @Transactional
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        Users users = usersDao.findByUsername(s);
        // 用户名找不到
        if (users == null) {
            log.info("用户名:[{}]不存在", s);
            throw new UsernameNotFoundException("用户名不存在");
        }
        // 获取该用户角色信息
        Set<Authorities> authoritiesSet = users.getAuthorities();
        ArrayList<GrantedAuthority> list = new ArrayList<>();
        for (Authorities authorities : authoritiesSet) {
            list.add(new SimpleGrantedAuthority(authorities.getAuthority()));
        }
        return new User(
                users.getUsername(),
                users.getPassword(),
                list);

    }
}

Spring security配置

@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true)  //开启security注解
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserDetailsService userDetailsService;
    @Autowired
    private PersistentTokenRepository persistentTokenRepository;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(authenticationProvider());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 关闭csrf
        http.csrf().disable();

        // 自定义登录页面
        http.formLogin()
                .loginPage("/loginPage")    // 登录页面的url
                .loginProcessingUrl("/login")// 登录访问的路径,不用自己处理逻辑只定义url即可
                .failureUrl("/exception")  // 登录失败时跳转的路径
                .defaultSuccessUrl("/index", true);  // 登录成功后跳转的路径
        // url 拦截与放行,除//loginPage、/hello、/exception、/*.jpg外的路径都拦截
        http.authorizeRequests()
                .antMatchers("/loginPage", "/hello", "/exception", "/*.jpg").permitAll()
                .anyRequest().authenticated();

        // 用户注销,
        http.logout().logoutUrl("/logout");
        // 记住密码(自动登录)
        http.rememberMe().tokenRepository(persistentTokenRepository).tokenValiditySeconds(60 * 60).userDetailsService(userDetailsService);
    }

    /*
     * 密码加密器
     * */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

    /*
     * 记住密码token存储
     * */
    @Bean
    public PersistentTokenRepository persistentTokenRepository(DataSource dataSource) {
        // 数据存储在数据库中
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        jdbcTokenRepository.setDataSource(dataSource);
        return jdbcTokenRepository;
    }

    /*
     * 登录的友好提示
     * */
    @Bean
    public AuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        // 显示用户找不到异常,默认不论用户名密码哪个错误都提示密码错误
        provider.setHideUserNotFoundExceptions(false);
        provider.setPasswordEncoder(passwordEncoder());
        provider.setUserDetailsService(userDetailsService);
        return provider;
    }

}

url拦截与放行的细节:

  • /loginPage : 登陆页面
  • /hello :测试放行的其他页面
  • /exception : 登录失败时跳转的url
  • /*.jpg : 测试静态文件放行,只要时.jpg结尾的都放行

MVC相关代码

Controller代码编写

@Controller
@Slf4j
public class HelloController {
    @ResponseBody
    @RequestMapping("/hello")
    public String hello(HttpServletRequest request) throws ServletException {
        return "hello";
    }

    /*
     * 登录页面
     * */
    @GetMapping("/loginPage")
    public String login() {
        return "login";
    }


    // security 认证异常处理
    @GetMapping("/exception")
    public String error(HttpServletRequest request) {
        // 获取spring security的AuthenticationException异常并抛出,由全局异常统一处理
        AuthenticationException exception = (AuthenticationException) WebUtils.getSessionAttribute(request, "SPRING_SECURITY_LAST_EXCEPTION");
        if (exception != null) {
            throw exception;
        }
        return "redirect:/loginPage";
    }

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


    @ResponseBody
    @GetMapping("/role/teacher")
    @Secured({"ROLE_teacher", "ROLE_admin"})
    public String teacher() {
        return "教师界面";
    }

    @ResponseBody
    @GetMapping("/role/admin")
    @Secured({"ROLE_admin"})
    public String admin() {
        return "管理员界面";
    }

    @ResponseBody
    @GetMapping("/role/student")
    @Secured({"ROLE_student", "ROLE_admin"})
    public String student() {
        return "学生界面";
    }
}

@Secured({"ROLE_teacher", "ROLE_admin"}) : @Secured为spring security内部注解表示需要角色为teacher或admin才能访问。

全局异常处理器

@ControllerAdvice
@Slf4j
public class MyExceptionHandler {
    @ExceptionHandler(RuntimeException.class)
    public ModelAndView exception(Exception e) {
        log.info(e.toString());
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.setViewName("error");

        if (e instanceof BadCredentialsException) { // 密码错误(为了友好提示)
            modelAndView.addObject("msg", "密码错误");
        } else if (e instanceof AccessDeniedException) { // 权限不足
            modelAndView.addObject("msg", e.getMessage());
        } else {  // 其他
            modelAndView.addObject("msg", e.getMessage());
        }
        return modelAndView;
    }
}

thymeleaf模板

index.html首页(登录成功后跳转的页面)

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<p>欢迎你,用户名:<span sec:authentication="name"></span>,当前角色(权限):<span sec:authentication="principal.authorities"></span></p>
<h1>登录成功!</h1>
<a th:href="@{/logout}">退出</a>
<ul>
    <li sec:authorize="hasAnyRole('ROLE_admin')"><a th:href="@{/role/admin}">管理员界面</a></li>
    <li sec:authorize="hasAnyRole('ROLE_admin,ROLE_teacher')"><a th:href="@{/role/teacher}">教师界面</a></li>
    <li sec:authorize="hasAnyRole('ROLE_admin,ROLE_student')"><a th:href="@{/role/student}">学生界面</a></li>
</ul>
</body>
</html>

细节:

  • xmlns:th="http://www.thymeleaf.org" 使用thymeleaf需要导入的标签
  • xmlns:sec="http://www.thymeleaf.org/extras/spring-security" 在thymeleaf中使用spring security的标签
  • <span sec:authentication="name"></span> 显示当前的用户名
  • <span sec:authentication="principal.authorities"></span> 显示当前的角色(权限信息)
  • <li sec:authorize="hasAnyRole('ROLE_admin')"><a th:href="@{/role/admin}">管理员界面</a></li> 当前登录的用户需要由admin角色信息才可以显示
    • hasAnyAuthority() 当前用户需要由xxx权限才能显示(本案例中未使用)
    • 如果有多个角色信息,可以用","隔开

login.html 登录页

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
    <meta charset="UTF-8">
    <title>登录</title>
</head>
<body>
<form action="/login" method="post">
    <h1>登录页面</h1>
    用户名:<input type="text" name="username"><br/>
    密码:<input type="password" name="password"><br/>
    <label for="remember-me">自动登录</label><input id="remember-me" type="checkbox" name="remember-me"><br/>
    <!--scrf开启时需要-->
    <!--    <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">-->
    <input type="submit" value="login">
    <p></p>
</form>
</body>
</html>

细节:

  • 用户名参数名默认情况下为username,密码参数名默认为password,可以通过配置文件修改
  • 记住密码参数名只能为remember-me
  • 开启csrf后需要在表单中添加一个隐藏域

error.html
通过全局异常处理把错误信息统一发送到该页面提示

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>错误提示</title>
</head>
<body>
<h1 th:text="${msg}"></h1>
</body>
</html>

给数据库添加用户信息

@SpringBootTest
class Demo1StartApplicationTests {
    @Autowired
    private UsersDao usersDao;
    @Autowired
    private PasswordEncoder passwordEncoder;

    // 用户名为admin ,角色信息为admin,密码均为123456
    @Test
    void contextLoads1() {
        Users users = new Users();
        users.setUsername("admin");
        users.setPassword(passwordEncoder.encode("123456"));
        Authorities authorities = new Authorities();
        authorities.setAuthority("ROLE_admin");

        users.getAuthorities().add(authorities);

        usersDao.save(users);
    }

    @Test
    void contextLoads2() {
        Users users = new Users();
        users.setUsername("teacher");
        users.setPassword(passwordEncoder.encode("123456"));
        Authorities authorities = new Authorities();
        authorities.setAuthority("ROLE_teacher");

        users.getAuthorities().add(authorities);

        usersDao.save(users);
    }

    @Test
    void contextLoads3() {
        Users users = new Users();
        users.setUsername("student");
        users.setPassword(passwordEncoder.encode("123456"));
        Authorities authorities = new Authorities();
        authorities.setAuthority("ROLE_student");

        users.getAuthorities().add(authorities);

        usersDao.save(users);
    }
}

在spring security中,角色和权限时统一处理的,同样的字符串如果时“ROLE_”开头就把他当成角色信息,否则就是权限信息。