携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第31天,点击查看活动详情
7.1 Spring Security
Spring Security 底层就是Filter
新创建一个 springsecuritydemo模块 先来学习一下 Spring Security
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
一引入这个包自动就会产生效果,并生成自动的登录页面和账号、密码,账号是 user,项目一启动会在控制台打印出密码。我们在使用的时候肯定不是使用它的登录界面而是使用自己的登录界面并且使用的应该也是自己数据库的账号、密码,那我们应该怎么做呢?我们开发的时候是在业务层,想做认证、授权应该是在业务层以及数据访问层做一些处理。
首先要处理的是User实体类,我们在做授权的时候,当前的User具备哪些权限,怎么体现,体现在 实体类User 的 Type 属性上(0-普通用户,1-超级管理员,2-版主),这个 Type字段就代表当前用户具备哪些权限了,当然我们这个比较简单,一个用户只能有一类权限,但是将来在用Spring Security做授权的时候,我们要的不是这个Type,我们要的是字符串,这个字符串能够明确表达你的权限的含义,所以我们需要定义出来这样的字符串,我们通常让User这个实体类实现UserDetails接口,这个接口中规定了一些方法需要去实现,
public class User implements UserDetails {
private int id;
private String username;
private String password;
private String salt;
private String email;
private int type;
private int status;
private String activationCode;
private String headerUrl;
private Date createTime;
// 为了以免影响阅读体验,get、set。toString方法没有粘,但在开发时是有的
// 返回true:账号未过期 返回false:账号已过期
@Override
public boolean isAccountNonExpired() {
return true; // 这里返回true,我们认证的账号默认不做过期的处理
}
// 返回true:账号未锁定
@Override
public boolean isAccountNonLocked() {
return true;
}
// 返回true:凭证未过期(凭证就是登陆成功的一个结果)
@Override
public boolean isCredentialsNonExpired() {
return true;
}
// 返回true:账号可用
@Override
public boolean isEnabled() {
return true;
}
// 返回权限,这个用户具备的权限要返回回去
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
// 一个用户可能有多个权限,所以返回的是一个集合
List<GrantedAuthority> list = new ArrayList<>();
// GrantedAuthority是一个接口,我们在往list装的时候要实现它
list.add(new GrantedAuthority() {//每个GrantedAuthority通过下面方法封装一个权限,多个权限就多加几个GrantedAuthority
@Override
public String getAuthority() {
// 对于这个项目,我们是通过User的type字段判断的
switch (type){
case 1:
return "ADMIN"; // 表示管理员的意思
default:
return "USER"; // 否则就是普通用户
}
}
});
return list;
}
}
然后我们需要让UserService实现UserDetailsService接口,这个接口需要我们实现 loadUserByUsername() 根据用户名查用户方法,
@Service
public class UserService implements UserDetailsService {
@Autowired
private UserMapper userMapper;
public User findUserByName(String username) {
return userMapper.selectByName(username);
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return this.findUserByName(username);
}
}
穿插一下转发
和重定向
的区别:
重定向
地址栏变成B的,A和B之间没有耦合
转发
地址栏还是A的,A和B之间有耦合
基本准备工作已经准备好了,下面就要利用Spring Security对整个系统进行认证以及授权,Spring Security并不需要我们去帖子、私信那些组件上挨个处理,它底层是基于Filter拦截大量的请求,我们只需要写一个类在一个类当中就能够解决所有的需求,也就是Spring Security的配置类,在这个类当中要注入UserService那个组件,因为 UserService实现了 UserDetailsService接口,而这个是Security底层要依赖的。在配的时候通常要重写它的三个方法,这三个方法都叫都叫configure,参数不一样,
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserService userService;
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/resources/**"); // 忽略静态资源的访问,不过滤静态资源
}
// 这个方法内部主要是做认证
// AuthenticationManager:认证的核心接口
// AuthenticationManagerBuilder:用于构建AuthenticationManager对象的工具
// ProviderManager:AuthenticationManager接口的默认实现类
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 内置的认证规则,底层做认证需要UserDetailsService这个接口才能查出账号判断登录的对不对
// passwordEncoder 表示对密码进行编码
// Pbkdf2PasswordEncoder 是一个加密工具,里面传一个 salt,会把传入的密码加上salt进行加密
// auth.userDetailsService(userService).passwordEncoder(new Pbkdf2PasswordEncoder("12345"));
// 数据的形态和上面不匹配使用下面这种形式自定义
// 自定义认证规则
// AuthenticationProvider: ProviderManager持有一组AuthenticationProvider,每个AuthenticationProvider负责一种认证.
// 委托模式: ProviderManager将认证委托给AuthenticationProvider.
auth.authenticationProvider(new AuthenticationProvider() {
// Authentication: 用于封装认证信息的接口,不同的实现类代表不同类型的认证信息.
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName();
String password = (String) authentication.getCredentials();
User user = userService.findUserByName(username);
if (user == null) {
throw new UsernameNotFoundException("账号不存在!");
}
password = CommunityUtil.md5(password + user.getSalt());
if (!user.getPassword().equals(password)) {
throw new BadCredentialsException("密码不正确!");
}
// principal: 主要信息; credentials: 证书; authorities: 权限;
return new UsernamePasswordAuthenticationToken(user, user.getPassword(), user.getAuthorities());
}
// 当前的AuthenticationProvider支持哪种类型的认证.
// 返回当前的接口支持的是哪种认证
@Override
public boolean supports(Class<?> aClass) {
// UsernamePasswordAuthenticationToken: Authentication接口的常用的实现类.代表的是账号密码认证
return UsernamePasswordAuthenticationToken.class.equals(aClass);
}
});
}
// 做授权
@Override
protected void configure(HttpSecurity http) throws Exception {
// 登录的相关配置
http.formLogin()
.loginPage("/loginpage") // 告诉它登录页面是谁
.loginProcessingUrl("/login") // 处理的路径,表单上必须配这个路径
.successHandler(new AuthenticationSuccessHandler() { // 成功
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.sendRedirect(request.getContextPath() + "/index"); // 成功的话跳转到首页
}
})
.failureHandler(new AuthenticationFailureHandler() { // 失败
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
request.setAttribute("error", e.getMessage()); // 失败的话返回给页面一些错误提示
request.getRequestDispatcher("/loginpage").forward(request, response);// 失败的话还回到登录页面
}
});
// 退出时的相关配置
http.logout()
.logoutUrl("/logout")
.logoutSuccessHandler(new LogoutSuccessHandler() {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.sendRedirect(request.getContextPath() + "/index");
}
});
// 授权配置:拥有哪个权限能够访问哪个路径
http.authorizeRequests()
.antMatchers("/letter").hasAnyAuthority("USER", "ADMIN")
.antMatchers("/admin").hasAnyAuthority("ADMIN")
.and().exceptionHandling().accessDeniedPage("/denied"); //访问哪个路径:处理权限不匹配或没有权限的错误
// 增加Filter,处理验证码
http.addFilterBefore(new Filter() {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
if (request.getServletPath().equals("/login")) {
String verifyCode = request.getParameter("verifyCode");
if (verifyCode == null || !verifyCode.equalsIgnoreCase("1234")) {
request.setAttribute("error", "验证码错误!");
request.getRequestDispatcher("/loginpage").forward(request, response);
return;
}
}
// 让请求继续向下执行,走到下个Filter,如果下面没有Filter就走到Servlet
filterChain.doFilter(request, response);
}
}, UsernamePasswordAuthenticationFilter.class); // 在哪个Filter之前加
// 记住我
http.rememberMe()
.tokenRepository(new InMemoryTokenRepositoryImpl()) // 把记到内存里
.tokenValiditySeconds(3600 * 24) // 过期时间,单位:秒
.userDetailsService(userService); // 得传UserServiceDetail(实现类也行)
}
}
login.html:
index.html