Spring Security “万一我随便输入一个账号,再随便输入一堆密码,就登录到了马云的淘宝号呢?”:yum:
“那阿里又要向社会输入工作十年的人才了。”:happy:
简介 目标 我们之前在众多的实验中,无论虽然对业务逻辑的处理和持久层的操作,都封装在后端,无法被轻易访问。但是,要调用的时候,还是需要通过前端的控制层,以及thymeleaf页面来完成,这个时候很多操作都可以从url头直接进行,而不加以限制,这样是不完善的。
粗俗一点讲,就是我们需要登录了才能访问页面,其他的请求将通通加以限制。
概念 Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架(简单说是对访问权限进行控制嘛)。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
可以简单地认为 Spring Security 是放在用户和 Spring 应用之间的一个安全屏障, 每一个 web 请求都先要经过 Spring Security 进行 Authenticate 和 Authoration 验证。
我们现在来使用Security框架,来制作需要登录验证的网站模型。
基础配置 security的理解的使用比较繁杂,我们先把基础配置和重点分开来讲:
依赖与设置 引入pom依赖: 1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-security</artifactId > </dependency >
配置yml: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 mybatis: mapper-locations: classpath:/mapper/*.xml type-aliases-package: com.example.security.pojo spring: datasource: username: root password: 123456 url: jdbc:mysql://localhost:3306/springtest?useSSL=false&serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf8 driver-class-name: com.mysql.cj.jdbc.Driver tomcat: max-idle: 10 max-wait: 10000 max-active: 50 initial-size: 5 server: port: 8888
这里还是使用mybatis来操作持久层
mapper 1 2 3 4 5 6 7 <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace ="com.example.security.dao.UserMapper" > <select id ="getUser" parameterType ="String" resultType ="user" > select * from t_user_roles where username =#{username} </select > </mapper >
mapper的设置,直接获取全部属性。
数据库
这是数据库的设置,注意roles形式是固定,必须有ROLE_ 前缀
必要的组件 这里将所有必要的组件类列出来,这里不是重点,但是很有必要。
实体类 1 2 3 4 5 6 7 8 9 10 11 12 @Data @Alias ("user" )public class User { private Long id; private String username; private String password; private String roles; }
dao层 1 2 3 4 5 public interface UserMapper { User getUser (String username) ; }
服务层 1 2 3 4 5 6 7 8 9 10 @Service public class UserService { @Autowired UserMapper userMapper; public User getUserName (String name) { return userMapper.getUser(name); } }
控制层 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 @Controller public class UserController { @RequestMapping ("/" ) public String index () { return "index" ; } @RequestMapping ("/login" ) public String login () { return "login" ; } @RequestMapping ("/mypage" ) public String my () { return "mypage" ; } @RequestMapping ("/admin" ) public String a () { return "admin" ; } @RequestMapping ("/logout" ) public String logoutpage (HttpServletRequest request, HttpServletResponse response) { Authentication auth= SecurityContextHolder.getContext().getAuthentication(); if (auth!=null ){ new SecurityContextLogoutHandler().logout(request,response,auth); } return "redirect:/login?logout" ; } }
这里列出了控制层要接收的所有映射,现在暂时不说明他们各自的作用,待会会结合Security的各个配置一并讲解出来。
Security——登录和注销 这里开始讲述,Security是如何将它们整合起来的。大家可能会发现,上面的Controller层代码并没有Service层的注入,那么控制层是如何从数据库获取验证的呢?这里由下而上,从Service的注入开始。
UserDetailsService Service层的注入在了一个接入了UserDetailsService接口的类里面。而这个类通过重写UserDetailsService的方法,来获取到数据库的用户信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 @Service public class MyUserDetailService implements UserDetailsService { @Autowired UserService userService; @Override public UserDetails loadUserByUsername (String s) throws UsernameNotFoundException { System.out.println("通过用户名加载用户" ); com.example.security.pojo.User user = userService.getUserName(s); if (user == null ) { throw new UsernameNotFoundException("用户不存在" ); } return new User(user.getUsername() ,user.getPassword(), createAuthority(user.getRoles())); } private List<SimpleGrantedAuthority> createAuthority (String roles) { String[] roleArray = roles.split("," ); List<SimpleGrantedAuthority> authorityList = new ArrayList<>(); for (String role : roleArray) { authorityList.add(new SimpleGrantedAuthority(role)); } return authorityList; } }
这里重写了loadUserByUsername方法,这个方法会返回一个User类,我们会使用服务层的API,从数据库读取数据,让我们的user获得各项信息,在结合自己定义的方法,把我们的Roles划分为一个ArrayList列表。再使用Security的User类去重新构造它,并且返回。
注意,返回的这个User类是属于:
1 import org.springframework.security.core.userdetails.User;
这里在下犯了一个错误,就是不应该直接把实体类的名称直接定义为User,这样导致两个User类产生了冲突,所以在loadUserByUsername方法开头的user实例,使用地址的方式去声明。
那么这个类的方法又会被谁调用呢?
这个方法会被继承了WebSecurityConfigurerAdapter的类所调用。我们首先要继承WebSecurityConfigurerAdapter,并重写两个方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 @EnableWebSecurity @Configuration public class SecurityConfiguration extends WebSecurityConfigurerAdapter { @Autowired DataSource dataSource; @Autowired MyUserDetailService myUserDetailService; @Autowired AuthenticationSuccessHandler authenticationSuccessHandler; @Override protected void configure (HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/" , "/index" ).permitAll() .antMatchers("/mypage/**" ).hasAnyRole("USER" ) .antMatchers("/admin/**" ).hasAnyRole("ADMIN" ) .anyRequest().authenticated() .and() .formLogin() .loginPage("/login" ).permitAll() .defaultSuccessUrl("/mypage" ) .usernameParameter("user" ) .passwordParameter("password" ) .permitAll() .and() .logout() .permitAll() .and() .exceptionHandling().accessDeniedPage("/error" ); } @Override protected void configure (AuthenticationManagerBuilder auth) throws Exception { auth .userDetailsService(myUserDetailService) .passwordEncoder(new MyPassWordEncoder()); } }
在重写了configure(AuthenticationManagerBuilder auth)方法后,调用auth的注册,我们就能把用户的信息注册到整个Servelet的上下文中,使其全局存在,注意这里的MyPassWordEncoder:
1 2 3 4 5 6 7 8 9 10 11 public class MyPassWordEncoder implements PasswordEncoder { @Override public String encode (CharSequence charSequence) { return charSequence.toString(); } @Override public boolean matches (CharSequence charSequence, String s) { return s.equals(charSequence.toString()); } }
是需要重写PasswordEncoder去实现的,这是为了完成Security的密码加密功能,这里暂时不做延伸。
然后重写的configure(HttpSecurity http)方法,这个方法就是用来拦截请求和注册的,非常的关键。
一开始,便使用了authorizeRequests,来配置和定义请求,使用antMatchers来定义页面和权限的信息。
.and()的作用是连接表示还有事务需要配置
使用.formLogin()去定义登录页面,对于需要权限的请求,都需要跳转到某个url进行登录操作,使用loginPage(“/login”),表示url,permitAll()表示全部需要权限的页面
最后的logout()表示注销的页面,这里默认为logout,但注销操作实际上要根据业务的需要去定义,一般不会直接定位到某个页面。
.exceptionHandling().accessDeniedPage(“/error”);表示出现权限不足时,需要跳转的页面。
这里就需要重新提起Controller层的操作了:
1 2 3 4 @RequestMapping ("/login" )public String login () { return "login" ; }
虽然我们定义了跳转页面,但是我们仍然需要对/login请求进行重新规划,在defaultSuccessUrl中,定义了登录成功后会跳转的URL,这就表示了,如果登录成功,则跳转到相应的URL页面,否则,仍然返回login页面。
注销的实现 重新看回Controller层的/logout页面。
1 2 3 4 5 6 7 8 9 10 11 12 @RequestMapping ("/logout" )public String logoutpage (HttpServletRequest request, HttpServletResponse response) { Authentication auth= SecurityContextHolder.getContext().getAuthentication(); if (auth!=null ){ new SecurityContextLogoutHandler().logout(request,response,auth); } return "redirect:/login?logout" ; }
这里我们接受到了一个请求,使用(HttpServletRequest request, HttpServletResponse response),去处理这请求。
首先看到SecurityContextHolder.getContext().getAuthentication(); 它获取了我们存在于服务器中的上下文,同时又获取到了该用户的名字,如果该用户存在,则new一个新的用户去相应logout页面,最后再重定向回来。重定向在thymeleaf中也有所提及,是重新访问url页面并执行方法的意思。
测试 开始测试,首先需要HTML页面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 <!DOCTYPE html > <html lang ="en" xmlns:th ="http://www.w3.org/1999/xhtml" > <head > <meta charset ="UTF-8" > <title > Title</title > </head > <body > <h1 > 登陆页面</h1 > <div th:if ="${param.error}" > 密码错误 </div > <div th:if ="${param.logout}" > 您已注销 </div > <form th:action ="@{/login}" action ="" method ="post" > 用户名:<input name ="user" /> <br > 密码:<input name ="password" > <br /> <input type ="checkbox" name ="remember-me" > 记住我<br /> <input type ="submit" value ="登陆" > </form > </body > </html >
登录界面
这个就是登录界面.。
权限页面 当我们登录了张三这个用户的身份后,将自动跳转到mypage页面
再尝试去访问admin页面。
于是点击退出
然后再次用管理员的账号去登录管理员界面
完成!余下HTML页面会在文章最后的源码连接中放出。
Security——记住我 在WebSecurityConfigurerAdapter,选择记住我,但是这个.rememberMe(),也是有着许许多多的层级关系的。
Cookies 我们直接配置中使用:
开启的rememberMe()功能,但是它是存储于一个全局的Cookies中的,它会存在一段时间,在这段时间中,即使你切到其他页面,再切回来,也不用登录,但是一旦清除了Cookies,那就必须重新登录了。
同时,除了通过自己去编辑注销之外,还可以使用Security中自带的注销方法,它还可以自定义注销成功跳转的目录。:
1 http.logout().logoutSuccessUrl("/" );
同时,在控制层中注释掉logout的映射关系,并在html页面上改成:
1 2 3 4 <h3 > 退出</h3 > <form th:action ="@{/logout}" method ="post" > <input type ="submit" value ="注销" > </form >
它就会提交一个logout请求,而Security就会自动识别到这个请求,来完成我们的注销功能
可以在浏览器中按F12,打开application 的cookies页面,查看jsession信息。
自定义Cookies 我们还可以使用自定义Cookies去存储用户信息,使我们在关闭浏览器后,还缓存固定的时间。
1 2 3 4 5 6 http.authorizeRequests() .and() .rememberMe() .key("uniqueAndSecret" ) .rememberMeCookieName("remember-me" ) .tokenValiditySeconds(60 );
重新添加这段代码后,我们就将用户信息缓存在了Cookies中。
使用.key(“uniqueAndSecret”),指定缓存的键名,.rememberMeCookieName(“remember-me”)指定Cookies名。
.tokenValiditySeconds(60);则是指定Cookies时间,这里使用60秒来进行测试,我们可以在测试后发现,它确实在关闭浏览器后,仍然仍然存在。
Token 但是使用Cookies还是有着弊端,那就是我们再使用什么清理垃圾软件清理缓存的时候,将全部被清除掉,或者对于不支持缓存的浏览器而言,也是极其致命的,于是,我们可以将数据放到数据库中。这样就没有问题了。
修改配置 1 2 3 4 5 6 http.authorizeRequests() .and() .rememberMe() .tokenRepository(persistentTokenRepository()) .tokenValiditySeconds(60 ) .userDetailsService(myUserDetailService);
增加这项配置,注意 .tokenRepository 将会注册一个处理方法:
Bean 1 2 3 4 5 6 7 8 9 @Bean public PersistentTokenRepository persistentTokenRepository () { JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl(); jdbcTokenRepository.setDataSource(dataSource); return jdbcTokenRepository; }
这里配置了PersistentTokenRepository,也就是Token的处理方法。当我们开启了这个后,可以访问页面,这个时候便可以再去观察数据库,你会发现:
这个便是我们的登录的信息,它会被存储在数据库里,它也会再时间过期后,将用户登录信息给无效化。
Security——定制跳转 我们也可以在登录成功后,定制跳转页面,使得我们拥有admin权限的用户直接跳转到后台管理页面,而user权限的则跳转默认的主页。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 @Autowired AuthenticationSuccessHandler authenticationSuccessHandler; @Override protected void configure (HttpSecurity http) throws Exception { System.out.println("AppSecurityConfigurer()调用-------" ); http.authorizeRequests() .antMatchers("/login" ,"/css/**" ,"/js/**" ,"/img/**" ).permitAll() .antMatchers("/" ,"/home" ).hasRole("USER" ) .antMatchers("/admin/**" ).hasAnyRole("ADMIN" ,"DBA" ) .anyRequest().authenticated() .and() .formLogin().loginPage("/login" ) .successHandler(authenticationSuccessHandler) .usernameParameter("loginName" ).passwordParameter("password" ) .and() .logout().permitAll() .and() .exceptionHandling().accessDeniedPage("/accessDenied" ); }
这便是我们重新写的http处理。注释写完整了各个位置的含义。
以下是AuthenticationSuccessHandler 处理方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 @Component public class AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { private RedirectStrategy redirectStrategy=new DefaultRedirectStrategy(); @Override protected void handle (HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { String targetUrl=determineTargetUrl(authentication); redirectStrategy.sendRedirect(request,response,targetUrl); } protected String determineTargetUrl (Authentication authentication) { String url="" ; Collection<? extends GrantedAuthority> authorities=authentication.getAuthorities(); List<String> roles=new ArrayList<String>(); for (GrantedAuthority a:authorities) { roles.add(a.getAuthority()); } if (isAdmin(roles)) { url="/admin" ; }else if (isUser(roles)){ url="/home" ; }else {url="/accessDenied" ; } System.out.println("url=" +url); return url; } private boolean isUser (List<String> roles) { if (roles.contains("ROLE_USER" )){ return true ; } return false ; } private boolean isAdmin (List<String> roles) { if (roles.contains("ROLE_ADMIN" )){ return true ; } return false ; } @Override public RedirectStrategy getRedirectStrategy () { return redirectStrategy; } @Override public void setRedirectStrategy (RedirectStrategy redirectStrategy) { this .redirectStrategy = redirectStrategy; } }
源码地址:
https://github.com/Antarctica000/SpringBoot/tree/master/security