目录
1.基本概念
1.1什么是认证
1.2什么是会话
1.3什么是授权
1.3RBAC(授权方案)
1.3.1基于角色的访问控制
1.3.1基于资源的访问控制
2.基于session的认证方式
2.1实现认证
2.2实现Session会话
2.3实现授权
1.基本概念
1.1什么是认证
认证是保护系统的隐私数据与资源,用户身份合法方可访问资源。
例子:初次使用微信需要注册成微信用户,然后输入用户密码方可登录微信,输入账户密码登录微信的过程就是认证。
1.2什么是会话
用户认证通过后,为了避免用户每次操作都进行认证可将用户信息保存在会话中。会话就是为了保存当前用户登录状态所提供的机制。常见有基于Session 、Token方式。
1.3什么是授权
授权是用户认证通过根据用户的权限来控制用户访问资源的过程,拥有资源的访问权限则正常访问。
1.3RBAC(授权方案)
1.3.1基于角色的访问控制
根据图上逻辑,授权代码如下:
if(主体.hasRole("总经理角色id")){
查询工资
}
如上图查询工资需要的角色变成总经理和部门经理:
if(主体.hasRole("总经理角色id") || 主体.hasRole("部门经理角色id")){
查询工资
}
根据上面的示例,需要修改角色权限时就需要修改授权相关的代码,系统可扩展性差。
1.3.1基于资源的访问控制
根据上图中判断,授权代码可表示为:
if(主体.hasRole("查询工资权限标识")){
查询工资
}
优点:系统设计时定义好查询工资的权限标识,即使查询工资的角色发生变化也不需要修改代码,系统扩展性强。
2.基于session的认证方式
2.1实现认证
认证实现类,根据用户名查找用户信息,并校验密码,这里模拟了两个用户:
@Service
public class AuthenticationServiceImpl implements AuthenticationService{
/**
* 用户认证,校验用户身份信息是否合法
*
* @param authenticationRequest 用户认证请求,账号和密码
* @return 认证成功的用户信息
*/
@Override
public UserDto authentication(AuthenticationRequest authenticationRequest) {
//校验参数是否为空
if(authenticationRequest == null
|| StringUtils.isEmpty(authenticationRequest.getUsername())
|| StringUtils.isEmpty(authenticationRequest.getPassword())){
throw new RuntimeException("账号和密码为空");
}
//根据账号去查询数据库,这里测试程序采用模拟方法
UserDto user = getUserDto(authenticationRequest.getUsername());
//判断用户是否为空
if(user == null){
throw new RuntimeException("查询不到该用户");
}
//校验密码
if(!authenticationRequest.getPassword().equals(user.getPassword())){
throw new RuntimeException("账号或密码错误");
}
//认证通过,返回用户身份信息
return user;
}
//根据账号查询用户信息
private UserDto getUserDto(String userName){
return userMap.get(userName);
}
//用户信息
private Map<String,UserDto> userMap = new HashMap<>();
{
Set<String> authorities1 = new HashSet<>();
authorities1.add("p1");//这个p1我们人为让它和/r/r1对应
Set<String> authorities2 = new HashSet<>();
authorities2.add("p2");//这个p2我们人为让它和/r/r2对应
userMap.put("zhangsan",new UserDto("1010","zhangsan","123","张三","133443",authorities1));
userMap.put("lisi",new UserDto("1011","lisi","456","李四","144553",authorities2));
}
}
登录Controller,对/login请求处理,它调用AuthenticationService完成认证并返回登录结果提示信息:
@RestController
public class LoginController {
@Autowired
AuthenticationService authenticationService;
/***
用户登录
* @param authenticationRequest 登录请求
* @return
* */
@RequestMapping(value = "/login",produces = "text/plain;charset=utf-8")
public String login(AuthenticationRequest authenticationRequest, HttpSession session){
UserDto userDto = authenticationService.authentication(authenticationRequest);
//存入session
session.setAttribute(UserDto.SESSION_USER_KEY,userDto);
return userDto.getUsername() +"登录成功";
}
}
2.2实现Session会话
会话是指用户登入系统后,系统会记住该用户的登录状态,他可以在系统连续操作直到退出系统的过程。
认证的目的是对系统资源的保护,每次对资源的访问,系统必须得知道是谁在访问资源,才能对该请求进行合法性 拦截。因此,在认证成功后,一般会把认证成功的用户信息放入Session中,在后续的请求中,系统能够从Session 中获取到当前用户,用这样的方式来实现会话机制。
(1)增加会话控制
首先在UserDto中定义一个SESSION_USER_KEY,作为Session中存放登录用户信息的key。
public static final String SESSION_USER_KEY = "_user";
然后修改LoginController,认证成功后,将用户信息放入当前会话。并增加用户登出方法,登出时将session置为 失效.
@RestController
public class LoginController {
@Autowired
AuthenticationService authenticationService;
/***
用户登录
* @param authenticationRequest 登录请求
* @return
* */
@RequestMapping(value = "/login",produces = "text/plain;charset=utf-8")
public String login(AuthenticationRequest authenticationRequest, HttpSession session){
UserDto userDto = authenticationService.authentication(authenticationRequest);
//存入session
session.setAttribute(UserDto.SESSION_USER_KEY,userDto);
return userDto.getUsername() +"登录成功";
}
/***
用户退出
* @param session 当前session对象
* @return
* */
@GetMapping(value = "/logout",produces = {"text/plain;charset=UTF-8"})
public String logout(HttpSession session){
session.invalidate();
return "退出成功";
}
}
(2)增加测试资源
修改LoginController,增加测试资源1,它从当前会话session中获取当前登录用户,并返回提示信息给前台。
@GetMapping(value = "/r/r1",produces = {"text/plain;charset=UTF-8"})
public String r1(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";
}
@GetMapping(value = "/r/r2",produces = {"text/plain;charset=UTF-8"})
public String r2(HttpSession session){
String fullname = null;
Object userObj = session.getAttribute(UserDto.SESSION_USER_KEY);
if(userObj != null){
fullname = ((UserDto)userObj).getFullname();
}else{
fullname = "匿名";
}
return fullname + " 访问资源2";
}
2.3实现授权
现在我们已经完成了用户身份凭证的校验以及登录的状态保持,并且我们也知道了如何获取当前登录用户(从 Session中获取)的信息,接下来,用户访问系统需要经过授权,即需要完成如下功能:
- 匿名用户(未登录用户)访问拦截:禁止匿名用户访问某些资源。
- 登录用户访问拦截:根据用户的权限决定是否能访问某些资源。
(1)增加权限数据
为了实现这样的功能,我们需要在UserDto里增加权限属性,用于表示该登录用户所拥有的权限,同时修改 UserDto的构造方法。
@Data
@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;
}
并在AuthenticationServiceImpl中为模拟用户初始化权限,其中张三给了p1权限,李四给了p2权限。
//用户信息
private Map<String,UserDto> userMap = new HashMap<>();
{
Set<String> authorities1 = new HashSet<>();
authorities1.add("p1");//这个p1我们人为让它和/r/r1对应
Set<String> authorities2 = new HashSet<>();
authorities2.add("p2");//这个p2我们人为让它和/r/r2对应
userMap.put("zhangsan",new UserDto("1010","zhangsan","123","张三","133443",authorities1));
userMap.put("lisi",new UserDto("1011","lisi","456","李四","144553",authorities2));
}
(2)增加测试资源
(3)实现授权拦截器
在interceptor包下定义SimpleAuthenticationInterceptor拦截器,实现授权拦截:
- 校验用户是否登录
- 校验用户是否拥有操作权限
@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(object == null){
//没有认证,提示登录
writeContent(response,"请登录");
}
UserDto userDto = (UserDto) object;
//请求的url
String requestURI = request.getRequestURI();
if( userDto.getAuthorities().contains("p1") && requestURI.contains("/r/r1")){
return true;
}
if( userDto.getAuthorities().contains("p2") && requestURI.contains("/r/r2")){
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();
}
}
- 在WebConfig中配置拦截器,匹配/r/**的资源为受保护的系统资源,访问该资源的请求进入 SimpleAuthenticationInterceptor拦截器。
@Autowired
SimpleAuthenticationInterceptor simpleAuthenticationInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(simpleAuthenticationInterceptor).addPathPatterns("/r/**");
}
正式生产项目中,我们往往会考虑使用第三方安全框架(如 spring security,shiro等安全框架)来实现认证 授权功能,因为这样做能一定程度提高生产力,提高软件标准化程度,另外往往这些框架的可扩展性考虑的非常全 面。但是缺点也非常明显,这些通用化组件为了提高支持范围会增加很多可能我们不需要的功能,结构上也会比较 抽象,如果我们不够了解它,一旦出现问题,将会很难定位。