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


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

    环境搭建

    导入坐标

    当前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>
    View Code

    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_”开头就把他当成角色信息,否则就是权限信息。

  • 相关阅读:
    052-141
    052-140
    052-139
    052-138
    需要做笔记的页面
    日期总是显示1900/01/01 0:00:00
    延迟加载的树控件
    (简单)关于summary的注释
    江南检测
    fineui动态添加用户控件
  • 原文地址:https://www.cnblogs.com/47Gamer/p/14423356.html
Copyright © 2020-2023  润新知