认证
认证就是登录,比如密码登录微信,扫码登录,指纹登录,扫脸登录等
会话
登录之后,那么请求资源的时候,不必每次请求都再次认证,那就需要建立会话。
建立会话有两种方式: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项目的过程
- 导入 mvc、servlet3.0依赖
- 创建applicationContext.xml、springmvc.xml配置文件对应的配置类
- 创建web.xml对应的配置类。
applicationContext.xml对应的配置类
@Configuration
@ComponentScan(basePackages = {"org.lizhen"},excludeFilters = {
@ComponentScan.Filter(type = FilterType.ANNOTATION,value = Controller.class)})
public class ApplicationConfig {
// 在此配置除了@Controller的bean。比如数据库配置,事务管理器,业务bean等
}
springmvc.xml对应的配置类
@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;
}
}
web.xml对应的配置类(不需要加@Configuration注解,项目执行的时候,会自动把这个类扫描到的)
public class SpringApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override // spring容器,相当于加载applicationContext.xml
protected Class<?>[] getRootConfigClasses() {
return new Class[] {ApplicationConfig.class};
}
@Override // ServletContext,相当于加载springmvc.xml
protected Class<?>[] getServletConfigClasses() {
return new Class[]{WebConfig.class};
}
@Override // url-mapping
protected String[] getServletMappings() {
return new String[]{"/"};
}
}
根据springmvc.xml对应的在 一级目录下创建webapp包,项目结构如下:
小结:
创建servlet3.0 项目,而且整合springmvc,框架就如同上面一样。
servlet3.0项目不需要写web.xml,而是采用上面的配置方法就行了。
但是该配的内容却必须配置。上面的springmvc配置类就相当于web.xml配置DispatcherServlet。以前的web.xml配置如下:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<servlet>
<servlet-name>annomvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:annomvc-servlet.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>annomvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<!-- <filter>-->
<!-- <filter-name>encodingdemo</filter-name>-->
<!-- <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>-->
<!-- <init-param>-->
<!-- <!– 这个参数名是固定的,就是encoding–>-->
<!-- <param-name>encoding</param-name>-->
<!-- <param-value>utf-8</param-value>-->
<!-- </init-param>-->
<!-- </filter>-->
<!-- <filter-mapping>-->
<!-- <filter-name>encodingdemo</filter-name>-->
<!-- <url-pattern>/*</url-pattern>-->
<!-- </filter-mapping>-->
<absolute-ordering />
</web-app>
springmvc.xml的文件内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd">
<!-- 自动扫描包,让指定包下的注解生效,由IOC容器统一管理-->
<context:component-scan base-package="com"/>
<!-- 要使@RequestMapping生效,必须向上下文中注册DefaultAnnotationHandlerMapping
和AnnotationMethodHandlerAdapter实例
这两个实例分别在类和方法级别处理
而annotation-driven配置会自动完成上面两个实例的注入-->
<mvc:default-servlet-handler/>
<!-- 视图解析器-->
<bean id="irvr" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/jsp/"/>
<property name="suffix" value=".jsp"/>
</bean>
<mvc:annotation-driven>
<mvc:message-converters register-defaults="true">
<bean class="org.springframework.http.converter.StringHttpMessageConverter">
<constructor-arg value="utf-8"/>
</bean>
<bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
<property name="objectMapper">
<bean class="org.springframework.http.converter.json.Jackson2ObjectMapperFactoryBean">
<property name="failOnEmptyBeans" value="false"/>
</bean>
</property>
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
</beans>
实现认证功能
- 前端携带用户名密码进行登录,后端收到后去数据库里查询相应的用户,查到便登录成功,查不到就返回查不到
别忘了在配置类里注册 请求映射地址
@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(); } } }
@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(); } }
springsecurity
一般要实现认证和授权,首先想到的就是 aop或filter。security也是使用的这种技术。
原理:基于过滤链。 很多FilterChainProxy的实例组成的责任链。
真正干活的两个实现类是:AccessDecisionManager和AuthenticationManager
认证流程
真正干活的是 DaoAuthenticationProvider,所以可以重写这个类。但是重写这个类太麻烦了。所以我们只重写获取用户信息的部分,即UserDetailService
重写UserDetailService
下面是两种自定义UserDetailService的方式
写到配置类里,通过@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; }
写一个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方式,具体如下
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";
}
会话控制
退出api
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()就可以实现权限的访问了。
但是一般不建议使用@Secured()注解,而是使用@PreAuthorize、@PostAuthorize注解
第一步:先开启注解
@EnableGlobalMethodSecurity(prePostEnabled = true)
第二步,使用这些注解即可
分布式认证方案
1.基于session方式的验证
优点:安全性较高
缺点:服务器上需要存储一份,不能适应多种多样的客户端(比如app、小程序、web等)、而且实现过程比较麻烦。比如用户在服务器1上创建了session实例,如果这时候去访问服务器2,并没有实例。需要通过把session放到一个专门的session服务器上来实现。另外,如果服务器1挂了,就不好办了。
2.基于token令牌的方式的验证
缺点:可能会被破译
优点:只要在客户端上存储,不必存储在服务器上。