实现功能:本案例中有三个用户,他们的角色分别为管理员、老师、学生。管理员可以访问任意页面,而老师和学生只能访问自己的页面。
环境搭建
导入坐标
当前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_”开头就把他当成角色信息,否则就是权限信息。