认证

认证就是登录,比如密码登录微信,扫码登录,指纹登录,扫脸登录等

会话

登录之后,那么请求资源的时候,不必每次请求都再次认证,那就需要建立会话。
建立会话有两种方式:1.session机制 2.token机制
session:当用户第一次登录成功之后,服务端会创建一个session,并且把JsessionId响应给客户端。存储在客户端的cookie里。
客户端再次请求的时候,就会携带者cookie来请求。服务端去检查有没有匹配的jsessionId,有就通过。

token:同样是第一次请求的时候,服务端会生成一个token,并且传给客户端,与session机制不同的是,客户端可以把token存储在任意地方,比如cookie里。比如自己存储域里。
而且服务端不会有相应的token,而是根据首次生成token的一些数据来判断这个token是不是合法的。

授权

比如微信登录成功之后,用户可以看朋友圈,发信息,但是如果没有绑定银行卡,就不能转账和发红包,这就是授权。用户不能访问没有被授权的功能。
即授权就是根据用户权限来管理用户访问资源的问题。

RBAC

基于角色的访问控制:role-based access control

if(主体.hasRole(总经理角色id)) { 删除员工 }
缺点:如果增加一个部门经理也可以 删除员工,就必须改变上面的代码。
if(主体.hasRole(总经理角色id) || 主体.hasRole(部门经理角色id) ) { 删除员工 }

基于资源的访问控制:resource-based access control

if( 主体.hasPermission(“删除员工权限标识”) ) { }
优点:有标识就可以了,不用修改源代码。

基于session的认证方式

如何创建servlet3.0 项目?

体验一下创建maven项目,并且将其编程servlet3.0项目的过程

  1. 导入 mvc、servlet3.0依赖
  2. 创建applicationContext.xml、springmvc.xml配置文件对应的配置类
  3. 创建web.xml对应的配置类。

applicationContext.xml对应的配置类

  1. @Configuration
  2. @ComponentScan(basePackages = {"org.lizhen"},excludeFilters = {
  3. @ComponentScan.Filter(type = FilterType.ANNOTATION,value = Controller.class)})
  4. public class ApplicationConfig {
  5. // 在此配置除了@Controller的bean。比如数据库配置,事务管理器,业务bean等
  6. }

springmvc.xml对应的配置类

  1. @Configuration
  2. @EnableWebMvc
  3. @ComponentScan(basePackages = "org.lizhen", includeFilters =
  4. {@ComponentScan.Filter(type = FilterType.ANNOTATION, value = Controller.class)})
  5. public class WebConfig implements WebMvcConfigurer {
  6. @Bean
  7. public InternalResourceViewResolver viewResolver() {
  8. InternalResourceViewResolver resolver = new InternalResourceViewResolver();
  9. resolver.setPrefix("/WEB-INF/view");
  10. resolver.setSuffix(".jsp");
  11. return resolver;
  12. }
  13. }

web.xml对应的配置类(不需要加@Configuration注解,项目执行的时候,会自动把这个类扫描到的)

  1. public class SpringApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
  2. @Override // spring容器,相当于加载applicationContext.xml
  3. protected Class<?>[] getRootConfigClasses() {
  4. return new Class[] {ApplicationConfig.class};
  5. }
  6. @Override // ServletContext,相当于加载springmvc.xml
  7. protected Class<?>[] getServletConfigClasses() {
  8. return new Class[]{WebConfig.class};
  9. }
  10. @Override // url-mapping
  11. protected String[] getServletMappings() {
  12. return new String[]{"/"};
  13. }
  14. }

根据springmvc.xml对应的在 一级目录下创建webapp包,项目结构如下:
image.png
小结:
创建servlet3.0 项目,而且整合springmvc,框架就如同上面一样。
servlet3.0项目不需要写web.xml,而是采用上面的配置方法就行了。
但是该配的内容却必须配置。上面的springmvc配置类就相当于web.xml配置DispatcherServlet。以前的web.xml配置如下:

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4. xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
  5. http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
  6. version="4.0">
  7. <servlet>
  8. <servlet-name>annomvc</servlet-name>
  9. <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
  10. <init-param>
  11. <param-name>contextConfigLocation</param-name>
  12. <param-value>classpath:annomvc-servlet.xml</param-value>
  13. </init-param>
  14. <load-on-startup>1</load-on-startup>
  15. </servlet>
  16. <servlet-mapping>
  17. <servlet-name>annomvc</servlet-name>
  18. <url-pattern>/</url-pattern>
  19. </servlet-mapping>
  20. <!-- <filter>-->
  21. <!-- <filter-name>encodingdemo</filter-name>-->
  22. <!-- <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>-->
  23. <!-- <init-param>-->
  24. <!-- &lt;!&ndash; 这个参数名是固定的,就是encoding&ndash;&gt;-->
  25. <!-- <param-name>encoding</param-name>-->
  26. <!-- <param-value>utf-8</param-value>-->
  27. <!-- </init-param>-->
  28. <!-- </filter>-->
  29. <!-- <filter-mapping>-->
  30. <!-- <filter-name>encodingdemo</filter-name>-->
  31. <!-- <url-pattern>/*</url-pattern>-->
  32. <!-- </filter-mapping>-->
  33. <absolute-ordering />
  34. </web-app>

springmvc.xml的文件内容如下:

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <beans xmlns="http://www.springframework.org/schema/beans"
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4. xmlns:context="http://www.springframework.org/schema/context"
  5. xmlns:mvc="http://www.springframework.org/schema/mvc"
  6. xsi:schemaLocation="http://www.springframework.org/schema/beans
  7. http://www.springframework.org/schema/beans/spring-beans.xsd
  8. http://www.springframework.org/schema/context
  9. http://www.springframework.org/schema/context/spring-context.xsd
  10. http://www.springframework.org/schema/mvc
  11. http://www.springframework.org/schema/mvc/spring-mvc.xsd">
  12. <!-- 自动扫描包,让指定包下的注解生效,由IOC容器统一管理-->
  13. <context:component-scan base-package="com"/>
  14. <!-- 要使@RequestMapping生效,必须向上下文中注册DefaultAnnotationHandlerMapping
  15. 和AnnotationMethodHandlerAdapter实例
  16. 这两个实例分别在类和方法级别处理
  17. 而annotation-driven配置会自动完成上面两个实例的注入-->
  18. <mvc:default-servlet-handler/>
  19. <!-- 视图解析器-->
  20. <bean id="irvr" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
  21. <property name="prefix" value="/WEB-INF/jsp/"/>
  22. <property name="suffix" value=".jsp"/>
  23. </bean>
  24. <mvc:annotation-driven>
  25. <mvc:message-converters register-defaults="true">
  26. <bean class="org.springframework.http.converter.StringHttpMessageConverter">
  27. <constructor-arg value="utf-8"/>
  28. </bean>
  29. <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
  30. <property name="objectMapper">
  31. <bean class="org.springframework.http.converter.json.Jackson2ObjectMapperFactoryBean">
  32. <property name="failOnEmptyBeans" value="false"/>
  33. </bean>
  34. </property>
  35. </bean>
  36. </mvc:message-converters>
  37. </mvc:annotation-driven>
  38. </beans>

实现认证功能

image.png

  1. 前端携带用户名密码进行登录,后端收到后去数据库里查询相应的用户,查到便登录成功,查不到就返回查不到
  2. 别忘了在配置类里注册 请求映射地址

    @Configuration
    @EnableWebMvc
    @ComponentScan(basePackages = "org.lizhen", includeFilters =
         {@ComponentScan.Filter(type = FilterType.ANNOTATION, value = Controller.class)})
    public class WebConfig implements WebMvcConfigurer {
    
     @Bean
     public InternalResourceViewResolver viewResolver() {
         InternalResourceViewResolver resolver = new InternalResourceViewResolver();
         resolver.setPrefix("/WEB-INF/view/");
         resolver.setSuffix(".jsp");
         return resolver;
     }
    
     @Override
     public void addViewControllers(ViewControllerRegistry registry) {
         registry.addViewController("/").setViewName("login");
     }
    }
    
    @RestController
    public class loginController {
    
     @Autowired
     private AuthenticationService authenticationService;
    
     @RequestMapping(value = "/login" ,produces = "text/plain;charset=utf-8")
     public String login(AuthenticaionRequest authenticaionRequest) {
         User user = authenticationService.authentication(authenticaionRequest);
         if (user == null) {
             return "此用户没有注册,登录失败";
         }else {
             return "欢迎您"+user.getUsername();
         }
     }
    }
    

    image.pngimage.pngimage.png

    @Service
    public class AuthenticationServiceImpl implements AuthenticationService {
     private static HashMap<String,User> map = new HashMap<>();
     @Override
     public User authentication(AuthenticaionRequest authenticaionRequest) {
         User user = map.get(authenticaionRequest.getUsername());
         return user;
     }
     static {  // 模拟数据库
         map.put("zs",new User("zs","123",1,"111"));
         map.put("ls",new User("ls","123",2,"222"));
     }
    }
    

    实现会话

    很简单,只需要接收httpSession参数,并且加上属性就可以了。

    @RestController
    public class loginController {
     @Autowired
     private AuthenticationService authenticationService;
     public static final String SESSION_USER_KEY = "_user";
     @RequestMapping(value = "/login" ,produces = "text/plain;charset=utf-8")
     public String login(AuthenticaionRequest authenticaionRequest, HttpSession session) {
         User user = authenticationService.authentication(authenticaionRequest);
         if (user == null) {
             return "此用户没有注册,登录失败";
         }else {
             session.setAttribute(SESSION_USER_KEY,user);
             return "欢迎您"+user.getUsername();
         }
     }
    }
    

    实现分配资源功能:使用拦截器实现

    下面这个就是配置了 只有有p1的才能访问 /r/r1,只有有p2才能访问/r/r2

    @Component
    public class AuthenticationInterceptor implements HandlerInterceptor{
     @Override
     public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
         User user = (User)request.getSession().getAttribute(LoginController.SESSION_USER_KEY);
         System.out.println(request.getRequestURI());
         System.out.println(request.getRequestURL());
         // URI是只有资源   URL是域名+端口+资源
         if(user.getAuthorities().contains("p1") && request.getRequestURI().contains("/r/r1")) {
             return true;
         }
         if(user.getAuthorities().contains("p2")&& request.getRequestURI().contains("/r/r2")) {
             return true;
         }
         writeContext(response,"没有权限,拒绝访问");
         return false;
     }
    
     public void writeContext(HttpServletResponse response,String str) throws IOException {
         response.setContentType("text/html;charset=utf-8");
         PrintWriter writer = response.getWriter();
         writer.write(str);
         writer.close();
     }
    }
    

    image.png

springsecurity

一般要实现认证和授权,首先想到的就是 aop或filter。security也是使用的这种技术。
原理:基于过滤链。 很多FilterChainProxy的实例组成的责任链。
真正干活的两个实现类是:AccessDecisionManager和AuthenticationManager
image.png

认证流程

image.png
真正干活的是 DaoAuthenticationProvider,所以可以重写这个类。但是重写这个类太麻烦了。所以我们只重写获取用户信息的部分,即UserDetailService

重写UserDetailService

下面是两种自定义UserDetailService的方式

  1. 写到配置类里,通过@Bean添加

    //自定义用户信息服务
    @Bean
    public UserDetailsService userDetailsService() {
      InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
      manager.createUser(User.withUsername("zs").password("123").authorities("p1").build());
      manager.createUser(User.withUsername("ls").password("123").authorities("p2").build());
      return manager;
    }
    
  2. 写一个UserDetailService的自定义实现类,并通过@Component添加到容器里。

    @Component
    public class SpringDataUserDetailService implements UserDetailsService {
     @Override
     public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
         UserDetails ud = User.withUsername("zs").password("321").authorities("p1").build();
         return ud;
     }
    }
    

重写加密方式

security的加密方式有很多种,比如不加密或使用BCtrypt方式,具体如下
image.png
BCtypet加密方式详解:(下面通过测试来了解一下这个加密方式)
如果密码相同,使用的盐值也相同,那么生成的密码也是相同的。

    @Test
    void testBCrypt() {
        String pw = BCrypt.hashpw("123", BCrypt.gensalt());
        System.out.println(BCrypt.gensalt());
        System.out.println(pw);
        //校验密码
        boolean checkpw = BCrypt.checkpw(pw, "$2a$10$4438GzNbFah5BBQ8bXeffOOmlqmsNn5ixhkiR0//CGUgmTb7rkITm");
        boolean checkpw2 = BCrypt.checkpw(pw, "$2a$10$VllTVlcUu1N/L.1lpNrh1uuDHyTvt5Q54S2JMWVquGz707w/3NOJ2");
        System.out.println(checkpw);
        System.out.println(checkpw2);
    }
    @Test
    void testBCrypt() {
        String pw = BCrypt.hashpw("123", BCrypt.gensalt());
        System.out.println(pw);
        //校验密码
        boolean checkpw = BCrypt.checkpw("123", "$2a$10$xdKJazgzH5Qpeu3MihLOVezeu/P8vHyCAHHulAC.H60vNPHHYutOO");
        boolean checkpw2 = BCrypt.checkpw("123", "$2a$10$VllTVlcUu1N/L.1lpNrh1uuDHyTvt5Q54S2JMWVquGz707w/3NOJ2");
        System.out.println(checkpw);  // true
        System.out.println(checkpw2); // true
    }

上面校验密码的两个 密码都是123通过不同的盐值生成的密码,结果,竟然!都能匹配成功!

一般存到数据库的密码都是加密后的。
校验密码的时候就采用上面说的这个方式:BCrypt.checkpw(“用户输入的密码”,数据库查出来的密码)

自定义认证页面

.formLogin()时开启表单验证
.loginProcessingUrl(“/test/login”)是登录的另一个地址。一般用于给ajax请求用的。
但是其实直接访问/login也可以。(不管是ajax还是浏览器都可以访问/login和/test/login)作用都是一样的,
都是跳到登录页面。
如果有自定义的form表单,那么登录成功之后,会跳转到其action指定的地址。(当然这个地址必须被放行,不然那会重复的跳到登录页面(比如index.html)让用户重新登录。)

如果我们没有处理form规定的action提交地址,就会出现找不到地址的错误。

同样的,如果我们没有设置自定义登录页面,那么一开始我们访问 localhost:8080/dd,会跳转到登录页面,登录成功之后,会跳到/dd页面,如果我们没有设置/dd页面,也是会出现找不到的错误。

几个小问题

1.之前不管我访问什么,都会说重定向次数过多。(很多次重定向到 /login)

情景: 没有自定义登录页面,放心了/login
http.formLogin()
.loginProcessingUrl(“/test/login”)
.permitAll()

@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers(“/login”);
}

然后访问 /on、/sd、或者任意其他路径,根本就不给我登录的机会,而是会出现重定向次数过多的错误
但是如果放行的不是/login,而是loginProcessingUrl规定的 /test/login,就不会出现这个错误。
目前不知道这个问题是为啥。

2.当我自定义页面,并且没有给 form的action指定的/test/loginsuccess放行的时候,每次我输入密码登录都会重定向到login.html。
也不知道为啥。

建立会话

security的会话信息保存在 SecurityContextHolder的SecurityContext(Security上下文)里,这个上下文和当前线程绑定(即当前用户开辟的线程绑定)
接下来获取这个会话对象,并且尝试从里面拿到用户名密码验证通过后封装到里面的用户信息。

    private String getUsername() {
        Authentication authentication =
                SecurityContextHolder.getContext().getAuthentication();
        Object principal = authentication.getPrincipal(); //获取用户信息
        if (principal == null) { // 当没有登录的时候,得到的就是null
            // 如果没有登录,就访问一些不需要登录就能访问的资源(如/r/r1),就会为null
            return "游客";
        }else {
            User user = (User) principal;
            return user.getUsername();
        }
    }

    @RequestMapping("/p1")
    public String test3() {
        return getUsername()+"拿到资源p1";
    }

会话控制

image.png
image.pngimage.pngimage.png

退出api

image.pngimage.pngimage.pngimage.png
logoutSuccessHandler是退出成功之后才会执行。
logoutHandler不管退出是否成功都会执行。

授权

授权有两种方式:
1种是web授权(通过给url设置权限),另1种是基于方法授权(在方法上添加注解)

基于web授权

从数据查询所有权限

现有角色表、用户表、权限表。怎么查询某个用户的权限?

一个用户可能对应于多个角色,一个角色可能有多种权限. 从角色用户表里根据用户id查出对应的所有角色id 在权限角色表里更具角色id查出对应的所有权限,组合一下。便是所有权限了。

select * from permission where per_id in {
    select per_id from role_permission where role_in in {
      select role_id from user_role where user_id = "1"
  }
}

代码实现根据资源或用户的访问控制

http.antMatchers(“test/vip”).hasAuthority(“p1”)
这里 hasAuthority()便是根据资源授权了。
如果是hasRole()就是根据角色授权。
根据前面说的最好使用根据资源授权而不是根据角色授权

基于方法的授权

需要首先在配置类上开启注解:@EnableGlobalMethodSecurity(securedEnabled = true)
然后就可以在方法上使用@Secured()就可以实现权限的访问了。
image.png

但是一般不建议使用@Secured()注解,而是使用@PreAuthorize、@PostAuthorize注解
第一步:先开启注解
@EnableGlobalMethodSecurity(prePostEnabled = true)
第二步,使用这些注解即可
image.png

分布式认证方案

1.基于session方式的验证
优点:安全性较高
缺点:服务器上需要存储一份,不能适应多种多样的客户端(比如app、小程序、web等)、而且实现过程比较麻烦。比如用户在服务器1上创建了session实例,如果这时候去访问服务器2,并没有实例。需要通过把session放到一个专门的session服务器上来实现。另外,如果服务器1挂了,就不好办了。
image.pngimage.png
2.基于token令牌的方式的验证
缺点:可能会被破译
优点:只要在客户端上存储,不必存储在服务器上。
image.png