比如:在初次使用微信前需要注册成为微信用户,然后输入账号和密码即可登录微信,输入账号和密码
登录微信的过程就是认证。
系统为什么要认证?
认证是为了保护系统的隐私数据与资源,用户的身份合法方可访问该系统的资源。
认证 :用户认证就是判断一个用户的身份是否合法的过程,用户去访问系统资源时系统要求验证用户的身份信
息,身份合法方可继续访问,不合法则拒绝访问。常见的用户身份认证方式有:用户名密码登录,二维码登录,手
机短信登录,指纹认证等方式。
用户认证通过后,为了避免用户重复认证,可将用户的信息保证在会话中。会话就是系统为了保持当前
用户的登录状态所提供的机制,常见的有基于session方式、基于token方式等。
基于session的认证方式如下图:
它的交互流程是,用户认证成功后,在服务端生成用户相关的数据保存在session(当前会话)中,发给客户端的
sesssion_id 存放到 cookie 中,这样用户客户端请求时带上 session_id 就可以验证服务器端是否存在 session 数
据,以此完成用户的合法校验,当用户退出系统或session过期销毁时,客户端的session_id也就无效了。
基于token方式如下图:
它的交互流程是,用户认证成功后,服务端生成一个token发给客户端,客户端可以放到 cookie 或 localStorage
等存储中,每次请求时带上 token,服务端收到token通过验证后即可确认用户身份。
基于session的认证方式由Servlet规范定制,服务端要存储session信息需要占用内存资源,客户端需要支持
cookie;基于token的方式则一般不需要服务端存储token,并且不限制客户端的存储方式,所以基于token的方式更适合。
授权: 授权是用户认证通过根据用户的权限来控制用户访问资源的过程,拥有资源的访问权限则正常访问,没有
权限则拒绝访问。
授权可简单理解为Who对What(which)进行How操作
Who,即主体(Subject),主体一般是指用户,也可以是程序,需要访问系统中的资源。
What,即资源(Resource),如系统菜单、页面、按钮、代码方法、系统商品信息、系统订单信息等。系统菜单、页面、按钮、代码方法都属于系统功能资源,对于web系统每个功能资源通常对应一个URL;系统商品信息、系统订单信息都属于实体资源(数据资源),实体资源由资源类型和资源实例组成,比如商品信息为资源类型,商品编号 为001的商品为资源实例。
How,权限/许可(Permission),规定了用户对资源的操作许可,权限离开资源没有意义,如用户查询权限、用户添加权限、某个代码方法的调用权限、编号为001的用户的修改权限等,通过权限可知用户对哪些资源都有哪些操作许可。
主体、资源、权限关系如下图:
主体、资源、权限相关的数据模型如下:
主体(用户id、账号、密码、…)
资源(资源id、资源名称、访问地址、…)
权限(权限id、权限标识、权限名称、资源id、…)
角色(角色id、角色名称、…)
角色和权限关系(角色id、权限id、…)
主体(用户)和角色关系(用户id、角色id、…)
主体(用户)、资源、权限关系如下图:
通常企业开发中将资源和权限表合并为一张权限表,如下:
资源(资源id、资源名称、访问地址、…)
权限(权限id、权限标识、权限名称、资源id、…)
合并为:
权限(权限id、权限标识、权限名称、资源名称、资源访问地址、…)
修改后数据模型之间的关系如下图:
如何实现授权?业界通常基于RBAC实现授权。
RBAC基于角色的访问控制(Role-Based Access Control)是按角色进行授权,比如:主体的角色为总经理可以查
询企业运营报表,查询员工工资信息等,访问控制流程如下:
根据上图中的判断逻辑,授权代码可表示如下:
if(主体.hasRole("总经理角色id")){
查询工资
}
如果上图中查询工资所需要的角色变化为总经理和部门经理,此时就需要修改判断逻辑为“判断用户的角色是否是
总经理或部门经理”,修改代码如下:
if(主体.hasRole("总经理角色id") || 主体.hasRole("部门经理角色id")){
查询工资
}
根据上边的例子发现,当需要修改角色的权限时就需要修改授权的相关代码,系统可扩展性差。
RBAC基于资源的访问控制(Resource-Based Access Control)是按资源(或权限)进行授权,比如:用户必须
具有查询工资权限才可以查询员工工资信息等,访问控制流程如下:
根据上图中的判断,授权代码可以表示为:
if(主体.hasPermission("查询工资权限标识")){
查询工资
}
优点:系统设计时定义好查询工资的权限标识,即使查询工资所需要的角色变化为总经理和部门经理也不需要修改
授权代码,系统可扩展性强。
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<groupId>com.sungroupId>
<artifactId>security-springmvcartifactId>
<version>1.0-SNAPSHOTversion>
<packaging>warpackaging>
<properties>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
<maven.compiler.source>1.8maven.compiler.source>
<maven.compiler.target>1.8maven.compiler.target>
properties>
<dependencies>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-webmvcartifactId>
<version>5.1.5.RELEASEversion>
dependency>
<dependency>
<groupId>javax.servletgroupId>
<artifactId>javax.servlet-apiartifactId>
<version>3.0.1version>
<scope>providedscope>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>1.18.8version>
dependency>
dependencies>
<build>
<finalName>security-springmvcfinalName>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.tomcat.mavengroupId>
<artifactId>tomcat7-maven-pluginartifactId>
<version>2.2version>
plugin>
<plugin>
<groupId>org.apache.maven.pluginsgroupId>
<artifactId>maven-compiler-pluginartifactId>
<configuration>
<source>1.8source>
<target>1.8target>
configuration>
plugin>
<plugin>
<groupId>org.apache.maven.pluginsgroupId>
<artifactId>maven-war-pluginartifactId>
<version>3.0.0version>
<configuration>
<failOnMissingWebXml>falsefailOnMissingWebXml>
configuration>
plugin>
<plugin>
<artifactId>maven-resources-pluginartifactId>
<configuration>
<encoding>utf-8encoding>
<useDefaultDelimiters>trueuseDefaultDelimiters>
<resources>
<resource>
<directory>src/main/resourcesdirectory>
<filtering>truefiltering>
<includes>
<include>**/*include>
includes>
resource>
<resource>
<directory>src/main/javadirectory>
<includes>
<include>**/*.xmlinclude>
includes>
resource>
resources>
configuration>
plugin>
plugins>
pluginManagement>
build>
project>
/**
* 相当于applicationContext.xml
*/
@Configuration
@ComponentScan(basePackages = "com.sun.security.springmvc"
,excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION,value = Controller.class)})
public class ApplicationConfig {
//在此配置除了Controller的其它bean,比如:数据库链接池、事务管理器、业务bean等。
}
/**
* 相当于springmvc.xml
*/
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = "com.sun.security.springmvc"
,includeFilters= {@ComponentScan.Filter(type = FilterType.ANNOTATION,value = Controller.class)})
public class WebConfig implements WebMvcConfigurer {
//视频解析器
@Bean
public InternalResourceViewResolver viewResolver(){
InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
viewResolver.setPrefix("/WEB-INF/view/");
viewResolver.setSuffix(".jsp");
return viewResolver;
}
}
/**
* Spring容器初始化,相当于web.xml
*/
public class SpringApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
//spring容器,相当于加载 applicationContext.xml
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class[]{ApplicationConfig.class};
}
//servletContext,相当于加载springmvc.xml
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class[]{WebConfig.class};
}
//url-mapping
@Override
protected String[] getServletMappings() {
return new String[]{"/"};
}
}
在webapp/WEB-INF/views下定义认证页面login.jsp
<%@ page contentType="text/html;charset=UTF-8" pageEncoding="utf-8" %>
<html>
<head>
<title>用户登录title>
head>
<body>
<form action="login" method="post">
用户名:<input type="text" name="username"><br>
密 码:
<input type="password" name="password"><br>
<input type="submit" value="登录">
form>
body>
html>
在WebConfig中新增如下配置,将/直接导向login.jsp页面:
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("login");
}
定义认证接口,此接口用于对传来的用户名、密码校验,若成功则返回该用户的详细信息,否则抛出错误异常:
/**
* 认证服务
*/
public interface AuthenticationService {
/**
* 用户认证
* @param authenticationRequest 用户认证请求,账号和密码
* @return 认证成功的用户信息
*/
UserDto authentication(AuthenticationRequest authenticationRequest);
}
实现类
/**
* 认证的实现类
*/
@Service
public class AuthenticationServiceImpl implements AuthenticationService {
@Override
public UserDto authentication(AuthenticationRequest authenticationRequest) {
//校验参数是否为空
if(null == authenticationRequest ||
StringUtils.isEmpty(authenticationRequest.getUsername()) ||
StringUtils.isEmpty(authenticationRequest.getPassword())){
throw new RuntimeException("账号或密码为空");
}
//根据账号去查询数据库,这里测试程序采用模拟方法
UserDto userDto = getUserDto(authenticationRequest.getUsername());
if(null == userDto){
throw new RuntimeException("查询不到该用户");
}
if(!userDto.getPassword().equals(authenticationRequest.getPassword())){
throw new RuntimeException("账号或密码错误");
}
return userDto;
}
//模拟用户查询
public UserDto getUserDto(String username){
return userMap.get(username);
}
//用户信息
private Map<String,UserDto> userMap = new HashMap<>();
{
userMap.put("zhangsan",new UserDto("1010","zhangsan","123","张三","133443"));
userMap.put("lisi",new UserDto("1011","lisi","456","李四","144553"));
}
}
请求头
@Data
@NoArgsConstructor
@AllArgsConstructor
public class AuthenticationRequest {
//认证请求参数,账号、密码。。
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
}
返回值
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserDto {
//用户身份信息
private String id;
private String username;
private String password;
private String fullname;
private String mobile;
}
登录Controller
/**
* 登录控制器
*/
@RestController
public class LoginController {
@Autowired
AuthenticationService authenticationService;
@RequestMapping(value = "/login",produces = "text/plain;charset=utf-8")
public String login(AuthenticationRequest authenticationRequest){
UserDto user = authenticationService.authentication(authenticationRequest);
return user.getUsername()+"登录成功";
}
}
会话是指用户登入系统后,系统会记住该用户的登录状态,他可以在系统连续操作直到退出系统的过程。
(1)UserDto中定义一个SESSION_USER_KEY,作为Session中存放登录用户信息的key。
public static final String SESSION_USER_KEY = "_user";
(2)修改LoginController,认证成功后,将用户信息放入当前会话。并增加用户登出方法,登出时将session置为失效。
//登录
@PostMapping(value = "/login", produces = "text/plain;charset=utf-8")
public String login(AuthenticationRequest authenticationRequest, HttpSession session){
UserDto user = authenticationService.authentication(authenticationRequest);
session.setAttribute(user.SESSION_USER_KEY,user);
return user.getUsername()+"登录成功";
}
@GetMapping(value = "/logout",produces = {"text/plain;charset=UTF-8"})
public String logout(HttpSession session){
session.invalidate();
return "退出成功";
}
(3)从session中获取用户信息
//从session中获取用户信息
@GetMapping(value = "/session/getUser",produces = {"text/plain;charset=UTF-8"})
public String getUser(HttpSession session){
String fullname = null;
Object object = session.getAttribute(UserDto.SESSION_USER_KEY);
if(object == null){
fullname = "匿名";
}else{
UserDto userDto = (UserDto) object;
fullname = userDto.getFullname();
}
return fullname+"访问资源r1";
}
(1)为了实现这样的功能,我们需要在UserDto里增加权限属性,用于表示该登录用户所拥有的权限,同时修改UserDto的构造方法。
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserDto {
public static final String SESSION_USER_KEY = "_user";
//用户身份信息
private String id;
private String username;
private String password;
private String fullname;
private String mobile;
/**
* 用户权限
*/
private Set<String> authorities;
}
(2)并在AuthenticationServiceImpl中为模拟用户初始化权限,其中张三给了p1权限,李四给了p2权限
//用户信息
private Map<String,UserDto> userMap = new HashMap<>();
{
Set<String> authorities1 = new HashSet<>();
authorities1.add("p1");
Set<String> authorities2 = new HashSet<>();
authorities2.add("p2");
userMap.put("zhangsan",new UserDto("1010","zhangsan","123","张三","133443",authorities1));
userMap.put("lisi",new UserDto("1011","lisi","456","李四","144553",authorities2));
}
(3)在LoginController中增加测试资源2
//从session中获取用户信息
@GetMapping(value = "/session/getUser2",produces = {"text/plain;charset=UTF-8"})
public String getUser2(HttpSession session){
String fullname = null;
Object object = session.getAttribute(UserDto.SESSION_USER_KEY);
if(object == null){
fullname = "匿名";
}else{
UserDto userDto = (UserDto) object;
fullname = userDto.getFullname();
}
return fullname+"访问资源r2";
}
(4)在interceptor包下定义SimpleAuthenticationInterceptor拦截器,实现授权拦截
1、校验用户是否登录
2、校验用户是否拥有操作权限
/**
* 拦截器
*/
@Component
public class SimpleAuthenticationInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//在这个方法中校验用户请求的url是否在用户的权限范围内
Object object = request.getSession().getAttribute(UserDto.SESSION_USER_KEY);
if(null == object){
//没有认证,提示登录
writeContent(response,"请登录");
}
UserDto userDto = (UserDto) object;
//请求的url
String requestURI = request.getRequestURI();
if(userDto.getAuthorities().contains("p1") && requestURI.contains("/session/getUser1")){
return true;
}
if(userDto.getAuthorities().contains("p2") && requestURI.contains("/session/getUser2")){
return true;
}
writeContent(response,"没有权限,拒绝访问");
return false;
}
//响应信息给客户端
private void writeContent(HttpServletResponse response, String msg) throws IOException {
response.setContentType("text/html;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.print(msg);
writer.close();
}
}
(5)在WebConfig中配置拦截器
@Autowired
SimpleAuthenticationInterceptor simpleAuthenticationInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(simpleAuthenticationInterceptor).addPathPatterns("/session/**");
}
<dependency>
<groupId>org.springframework.securitygroupId>
<artifactId>spring-security-webartifactId>
<version>5.1.4.RELEASEversion>
dependency>
<dependency>
<groupId>org.springframework.securitygroupId>
<artifactId>spring-security-configartifactId>
<version>5.1.4.RELEASEversion>
dependency>
/**
* 相当于applicationContext.xml
*/
@Configuration
@ComponentScan(basePackages = "com.sun.security.springmvc"
,excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION,value = Controller.class)})
public class ApplicationConfig {
//在此配置除了Controller的其它bean,比如:数据库链接池、事务管理器、业务bean等。
}
/**
* 相当于springmvc.xml
*/
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = "com.sun.security.springmvc"
,includeFilters= {@ComponentScan.Filter(type = FilterType.ANNOTATION,value = Controller.class)})
public class WebConfig implements WebMvcConfigurer {
//视频解析器
@Bean
public InternalResourceViewResolver viewResolver(){
InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
viewResolver.setPrefix("/WEB-INF/view/");
viewResolver.setSuffix(".jsp");
return viewResolver;
}
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("redirect:/login");
}
}
/**
* Spring容器初始化,相当于web.xml
*/
public class SpringApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
//spring容器,相当于加载 applicationContext.xml
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class[]{ApplicationConfig.class, WebSecurityConfig.class};
}
//servletContext,相当于加载springmvc.xml
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class[]{WebConfig.class};
}
//url-mapping
@Override
protected String[] getServletMappings() {
return new String[]{"/"};
}
}
springSecurity默认提供认证页面,不需要额外开发。
spring security提供了用户名密码登录、退出、会话管理等认证功能,只需要配置即可使用。
/**
* 安全配置(spring security)
*/
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
//定义用户信息服务(查询用户信息)
@Bean
public UserDetailsService userDetailsService(){
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("zhangsan").password("123").authorities("p1").build());
manager.createUser(User.withUsername("lisi").password("456").authorities("p2").build());
return manager;
}
//密码编码器
@Bean
public PasswordEncoder passwordEncoder(){
return NoOpPasswordEncoder.getInstance();
}
//安全拦截机制(最重要)
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/session/r1").hasAuthority("p1")
.antMatchers("/session/r2").hasAuthority("p2")
.antMatchers("/session/**").authenticated()//所有/session/**的请求必须认证通过
.anyRequest().permitAll()//除了/session/**,其他请求可以访问
.and()
.formLogin()//运行表单登录
.successForwardUrl("/login-success");//登录访问接口
}
}
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class<?>[] { ApplicationConfig.class, WebSecurityConfig.class};
}
Spring Security初始化,这里有两种情况
在init包下定义SpringSecurityApplicationInitializer:
public class SpringSecurityApplicationInitializer
extends AbstractSecurityWebApplicationInitializer {
public SpringSecurityApplicationInitializer() {
//super(WebSecurityConfig.class);
}
}
在WebConfig.java中添加默认请求根路径跳转到/login,此url为spring security提供:
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("redirect:/login");
}
在安全配置中,认证成功将跳转到/login-success,代码如下:
//安全拦截机制(最重要)
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/session/r1").hasAuthority("p1")
.antMatchers("/session/r2").hasAuthority("p2")
.antMatchers("/session/**").authenticated()//所有/session/**的请求必须认证通过
.anyRequest().permitAll()//除了/session/**,其他请求可以访问
.and()
.formLogin()//运行表单登录
.successForwardUrl("/login-success");//登录访问接口
}
spring security支持form表单认证,认证成功后转向/login-success。
在LoginController中定义/login-success:
@RequestMapping(value = "/login-success",produces = {"text/plain;charset=UTF-8"})
public String loginSuccess(){
return " 登录成功";
}
在LoginController添加/session/r1或/session/r2
/**
* 测试资源1
* @return
*/
@GetMapping(value = "/session/r1",produces = {"text/plain;charset=UTF-8"})
public String r1(){
return " 访问资源1";
}
/**
* 测试资源2
* @return
*/
@GetMapping(value = "/session/r2",produces = {"text/plain;charset=UTF-8"})
public String r2(){
return " 访问资源2";
}
在安全配置类WebSecurityConfig.java中配置
//安全拦截机制(最重要)
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/session/r1").hasAuthority("p1")
.antMatchers("/session/r2").hasAuthority("p2")
.antMatchers("/session/**").authenticated()//所有/session/**的请求必须认证通过
.anyRequest().permitAll()//除了/session/**,其他请求可以访问
.and()
.formLogin()//运行表单登录
.successForwardUrl("/login-success");//登录访问接口
}