出于某些原因,需要学习一下spring的安全框架。(研究半天,如果单单说用户认证和授权这块儿,我感觉还是shiro好用。)
spring security介绍可以参考一下以下文档:
(满满的羡慕啊)我这里就不扯了。
http://www.tianshouzhi.com/api/tutorials/spring_security_4/252
一、先贴代码
下边是我的项目结构
当然我采用的是springboot + thymeleaf + mybatis + mysql
1.用到的pom.xml
1 <dependency> 2 <groupId>org.springframework.boot</groupId> 3 <artifactId>spring-boot-starter-web</artifactId> 4 </dependency> 5 6 <dependency> 7 <groupId>org.springframework.boot</groupId> 8 <artifactId>spring-boot-starter-test</artifactId> 9 <scope>test</scope> 10 </dependency> 11 12 <dependency> 13 <groupId>org.mybatis.spring.boot</groupId> 14 <artifactId>mybatis-spring-boot-starter</artifactId> 15 <version>2.0.1</version> 16 </dependency> 17 18 <!-- mysql --> 19 <dependency> 20 <groupId>mysql</groupId> 21 <artifactId>mysql-connector-java</artifactId> 22 <version>5.1.32</version> 23 </dependency> 24 25 <!-- lombok --> 26 <dependency> 27 <groupId>org.projectlombok</groupId> 28 <artifactId>lombok</artifactId> 29 <version>1.18.8</version> 30 <scope>provided</scope> 31 </dependency> 32 33 <!-- security+thymeleaf --> 34 <dependency> 35 <groupId>org.springframework.boot</groupId> 36 <artifactId>spring-boot-starter-security</artifactId> 37 <version>2.1.5.RELEASE</version> 38 </dependency> 39 40 <dependency> 41 <groupId>org.springframework.boot</groupId> 42 <artifactId>spring-boot-starter-thymeleaf</artifactId> 43 <!-- <version>2.1.3.RELEASE</version>--> 44 </dependency> 45 <dependency> 46 <groupId>org.thymeleaf.extras</groupId> 47 <artifactId>thymeleaf-extras-springsecurity4</artifactId> 48 <version>3.0.2.RELEASE</version> 49 </dependency>
2.核心配置类
1 import org.springframework.context.annotation.Bean; 2 import org.springframework.context.annotation.Configuration; 3 import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 4 import org.springframework.security.config.annotation.web.builders.HttpSecurity; 5 import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 6 import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 7 import org.springframework.security.core.userdetails.UserDetailsService; 8 import org.springframework.security.crypto.password.MessageDigestPasswordEncoder; 9 10 @Configuration 11 @EnableWebSecurity 12 public class WebSecurityConfig extends WebSecurityConfigurerAdapter { 13 @Bean 14 UserDetailsService customUserService(){ //注册UserDetailsService 的bean 15 return new UserDetailsServiceImpl(); 16 } 17 18 @Override 19 protected void configure(AuthenticationManagerBuilder auth) throws Exception { 20 auth.userDetailsService(customUserService()).passwordEncoder(new MessageDigestPasswordEncoder("MD5")); //user Details Service验证 21 } 22 @Override 23 protected void configure(HttpSecurity http) throws Exception { 24 http.authorizeRequests() 25 .anyRequest().authenticated() //authenticated任何请求,登录后可以访问 26 .and() 27 .csrf().disable() 28 .formLogin() 29 .loginPage("/user/login")// 30 .defaultSuccessUrl("/user/index") 31 .failureUrl("/user/login?error=true") 32 .permitAll() //登录页面用户任意访问 33 .and() 34 .logout().permitAll(); //注销行为任意访问 35 } 36 }
3.UserDetailsServiceImpl
1 import com.qx.demo.entity.RolePo; 2 import com.qx.demo.entity.UserPo; 3 import com.qx.demo.service.UserService; 4 import org.springframework.beans.factory.annotation.Autowired; 5 import org.springframework.security.core.authority.SimpleGrantedAuthority; 6 import org.springframework.security.core.userdetails.User; 7 import org.springframework.security.core.userdetails.UserDetails; 8 import org.springframework.security.core.userdetails.UserDetailsService; 9 import org.springframework.security.core.userdetails.UsernameNotFoundException; 10 11 import java.util.HashSet; 12 import java.util.Set; 13 14 public class UserDetailsServiceImpl implements UserDetailsService { 15 @Autowired 16 UserService service; 17 @Override 18 public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException { 19 UserPo user = service.getUserById(s); 20 if (user==null){ 21 throw new UsernameNotFoundException("用户名不存在"); 22 } 23 Set<SimpleGrantedAuthority> authorities = new HashSet<>(); 24 for (RolePo item:user.getRoles()){ 25 authorities.add(new SimpleGrantedAuthority(item.getRName())); 26 System.out.println(item.getRName()); 27 } 28 return new User(user.getUsername(),user.getPassword(),authorities); 29 } 30 }
4.控制层Controller
1 import com.qx.demo.service.UserService; 2 import org.springframework.beans.factory.annotation.Autowired; 3 import org.springframework.security.core.Authentication; 4 import org.springframework.security.core.userdetails.User; 5 import org.springframework.stereotype.Controller; 6 import org.springframework.web.bind.annotation.RequestMapping; 7 import org.springframework.web.servlet.ModelAndView; 8 9 import javax.servlet.http.HttpServletRequest; 10 11 @Controller 12 @RequestMapping("user") 13 public class UserController { 14 @Autowired 15 private UserService service; 16 @RequestMapping("login") 17 public ModelAndView toLogin(HttpServletRequest request){ 18 ModelAndView mv= new ModelAndView("login"); 19 String error = request.getParameter("error"); 20 if (error!=null && error.equals("true")){ 21 System.out.println("登录失败"); 22 } 23 return mv; 24 } 25 @RequestMapping("index") 26 public String getUserById( Authentication authentication){ 27 User principal = (User)authentication.getPrincipal(); 28 if (authentication!=null) 29 System.out.println(authentication.getCredentials()+", "+authentication.getDetails()+", "+authentication.getPrincipal()+", "+authentication.getName()); 30 return "index"; 31 } 32 }
5.实体类
1 import lombok.Data; 2 import lombok.ToString; 3 4 import java.util.List; 5 6 @Data 7 @ToString 8 public class UserPo { 9 private int uid; 10 private String username; 11 private String password; 12 private List<RolePo> roles; 13 }
1 import lombok.Data; 2 import lombok.ToString; 3 4 import java.util.List; 5 6 @Data 7 @ToString 8 public class RolePo { 9 private int rid; 10 private String rName; 11 private List<PermissionPo> pers; 12 }
1 import lombok.Data; 2 import lombok.ToString; 3 4 @Data 5 @ToString 6 public class PermissionPo { 7 private int pid; 8 private String pName; 9 private String pPer; 10 }
6.Dao层
1 import com.qx.demo.entity.UserPo; 2 3 public interface UserMapper { 4 UserPo getUserByUsername(String username); 5 }
1 <?xml version="1.0" encoding="UTF-8"?> 2 <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" 3 "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> 4 <mapper namespace="com.qx.demo.dao.UserMapper"> 5 6 <resultMap id="getRoleAndPer" type="com.qx.demo.entity.UserPo"> 7 <id column="uid" property="uid"></id> 8 <result column="username" property="username"></result> 9 <result column="password" property="password"></result> 10 <collection property="roles" ofType="com.qx.demo.entity.RolePo"> 11 <id column="rid" property="rid"></id> 12 <result column="r_name" property="rName"></result> 13 <collection property="pers" ofType="com.qx.demo.entity.PermissionPo"> 14 <id column="pid" property="pid"></id> 15 <result column="p_name" property="pName"></result> 16 <result column="p_per" property="pPer"></result> 17 </collection> 18 </collection> 19 </resultMap> 20 <select id="getUserByUsername" parameterType="String" resultMap="getRoleAndPer"> 21 SELECT * FROM qx_user qu 22 INNER JOIN `qx_user_role` qur ON qu.`uid`=qur.`u_id` 23 INNER JOIN qx_role qr ON qur.`r_id` = qr.`rid` 24 INNER JOIN qx_role_per qrp ON qr.`rid`=qrp.`pid` 25 INNER JOIN qx_permission qp ON qrp.`pid`=qp.`pid` 26 WHERE username=#{_parameter} 27 </select> 28 </mapper>
7.service层
1 import com.qx.demo.entity.UserPo; 2 3 public interface UserService { 4 UserPo getUserById(String id); 5 }
1 import com.qx.demo.dao.UserMapper; 2 import com.qx.demo.entity.UserPo; 3 import com.qx.demo.service.UserService; 4 import org.springframework.stereotype.Service; 5 6 import javax.annotation.Resource; 7 8 @Service 9 public class UserServiceImpl implements UserService { 10 @Resource 11 private UserMapper mapper; 12 @Override 13 public UserPo getUserById(String username) { 14 return mapper.getUserByUsername(username); 15 } 16 }
8.application.properties
1 mybatis.mapper-locations=/mapper/*.xml 2 mybatis.type-aliases-package=com.qx.demo.entity 3 4 spring.datasource.driver-class-name=com.mysql.jdbc.Driver 5 spring.datasource.url=jdbc:mysql:///qx 6 spring.datasource.username=root 7 spring.datasource.data-password=root 8 9 spring.thymeleaf.mode=HTML5 10 spring.thymeleaf.cache=false 11 12 logging.level.com.qx.demo.dao=debug
二、再来看看spring-security的大概的执行流程
1.从上边图不难看出,spring-security的核心应该就是ProviderManager,但是ProviderManager并不直接执行认证和授权等相关操作,而是委托给AuthenticationProvider去完成相关操作,而对于每一个ProviderManager都是可以有多个AuthenticationProvider的。
2.AuthenticationProvider首先会找到UserDetailsService(有我们自己定义的)在数据库中查找用户信息。如果没有找到将会返回一个空的UserDetails,由于UserDetails是一个接口,我是在实体类中实现了该接口(那么这样我们就可以返回一个空壳user对象了)。
当然,如果找到了我们可以直接授权并将UserDetailsService返回。
3.AuthenticationProvider就收到一个非空的UserDetailsService,接下来就该进行密码比对了。这时AuthenticationProvider将会调用PasswordEncoder进行密码比对。
4.无论成功与否,spring-security都会在对应的过滤器上找到响应的响应方式,并响应给客户。
Spring-security和shiro一样,都是基于过滤器,所以我们免不了去配置一些过滤器:
当然,在前边的代码中已经体现过,该配置应该是在spring-security的核心配置类中进行配置(如果是xml也是一样的,大同小异嘛)。
上图是基于前后端分离,给前端返回一个json格式响应体的,比如登录成功时:
如图所示我们只需要实现AuthenticationSuccessHandler接口,并重写onAuthenticationSuccess方法即可。这个方法体完全就是一个类似与Servlet的方法体,有一定javaee基础的应该都没啥问题的。同样的道理如果登陆失败我们可以实现AuthenticationFailureHandler等等。
嗯,可能我忘了记录一个至关重要的玩意了,没错就是他:
你没看错,这就是一个基于用户名和密码的filter,spring-security拼什么这么智能,其实很简单就是一系列过滤器的使用。在源码中我们不难看出spring-security是设置了默认的登录地址的 /login,并且请求只允许POST请求。同时他也是spring-security默认登录过滤器。由于Java的开放原则的原因,你要是看他不爽你也可以自定义一个过滤器,你只需要继承一下AbstractAuthenticationProcessingFilter,重写其中的一些方法就好了。一般情况下我觉得默认的就可以了。
同样的与shiro雷同UsernamePasswordAuthenticationToken就是一个主体,当然保存的是客户端输入的登录信息。他最终是与UserDetailsService(在数据库中查到的信息)相对比得到登录结果。
对于用户信息来说我想密码应该是最重要的了,那么我们现在可以看看PasswordEncoder了
以上是security5中所有的PasswordEncoder了,当然,上边截图中有一个是我重写的PasswordEncoder(MyPasswordEncoder是我重写的)。这里我就不解释这些密码比对器都是什么作用了,如果有不了解的大家可以自己下去查询一下资料。我们公司要求的用的是MD5加密的方式,由于它是普通的散列,上网一百度就能看到答案,所以我需要重写一下PasswordEncoder。
同样的我们需要实现这个接口,我们需要重写两个方法encode()、matches()。一眼看上去不难发现,matches一定是用户认证的,那我们就可以把重点放到这个方法下了。
因为是例子,所以我写的比较简单了,显而易见我用的Spring自带的MD5加密工具了。
当然,spring-security是比较推荐 BCryptPasswordEncoder,我也象征性的看了一下源码并不是很难理解大家可以去看下。