return (httpServletRequest, httpServletResponse, e)->{
Map<String,Object> map=new HashMap<>();
map.put(“state”,401);//SC_UNAUTHORIZED 的值为401
map.put(“message”,“请先登录再访问”);
WebUtils.writeJsonToClient(httpServletResponse,map);
};
}
}
定义业务对象,处理客户端的登陆请求,例如:
package sso.auth.service;
@Service
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
private BCryptPasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//1.基于用户名从数据库查询用户信息
//SysUser user=userMapper.selectUserByUsername(username);//查数据库
if(!“jack”.equals(username))//假设这是从数据库查询的信息
throw new UsernameNotFoundException(“user not exists”);
//2.将用户信息封装到UserDetails对象中并返回
//假设这个密码是从数据库查询出来的
String encodedPwd=passwordEncoder.encode(“123456”);
//假设这个权限信息也是从数据库查询到的
//List permissions=userMapper.selectUserPermissions(username);//查数据库
//假如分配权限的方式是角色,编写字符串时用"ROLE_"做前缀
List grantedAuthorities =
AuthorityUtils.commaSeparatedStringToAuthorityList( “sys:res:retrieve,sys:res:create”);
//这个user是SpringSecurity提供的UserDetails接口的实现,用于封装用户信息
//后续我们也可以基于需要自己构建UserDetails接口的实现
User user=new User(username,encodedPwd,grantedAuthorities);
return user;//这里的返回值会交给springsecurity去校验
}
}
此对象编写好以后,可以启动服务基于postman进行登陆访问测试。
================================================================
第一步:定义JWT工具类,主要用于解析JWT令牌,例如
package sso.resource.util;
public class JwtUtils {
private static String secret=“AAABBBCCCDDDEEE”;
/*基于负载和算法创建token信息/
public static String generatorToken(Map<String,Object> map){
return Jwts.builder()
.setClaims(map)
.setExpiration(new Date(System.currentTimeMillis()+30601000))
.setIssuedAt(new Date())
.signWith(SignatureAlgorithm.HS256,secret)
.compact();//签约,创建token
}
/*解析token获取数据/
public static Claims getClaimsFromToken(String token){
return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
}
/*判定token是否失效/
public static boolean isTokenExpired(String token){
Date expiration=getClaimsFromToken(token).getExpiration();
return expiration.before(new Date());
}
}
第二步:定义Web工具类,用于向客户端响应json数据
package sso.auth.util;
public class WebUtils {
public static void writeJsonToClient(HttpServletResponse response, Map<String,Object> map)
throws IOException {
//1设置响应数据的编码
response.setCharacterEncoding(“utf-8”);
//2告诉浏览器响应数据的内容类型以及编码
response.setContentType(“application/json;charset=utf-8”);
//3获取输出流对象
PrintWriter out=response.getWriter();
//4 将map转换为json数据
String result=new ObjectMapper().writeValueAsString(map);
//5 将数据响应到客户端
out.println(result);
out.flush();
}
}
在资源服务中定义权限配置类,默认将所有认证请求放行,例如:
package sso.resource.config;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.exceptionHandling()
.accessDeniedHandler(accessDeniedHandler());
http.authorizeRequests().anyRequest().permitAll();
}
//没有权限时执行此处理器方法
public AccessDeniedHandler accessDeniedHandler(){
return (httpServletRequest, httpServletResponse, e)-> {
Map<String,Object> map=new HashMap<>();
map.put(“state”,403);//SC_FORBIDDEN的值是403
map.put(“message”,“没有访问权限,请联系管理员”);
WebUtils.writeJsonToClient(httpServletResponse,map);
};
}
}
定义资源服务对象,用于处理客户端的资源访问服务,例如:
package sso.resource.controller;
@RestController
public class ResourceController {
@PreAuthorize(“hasAuthority(‘sys:res:create’)”)
@RequestMapping(“/doCreate”)
public String doCreate(HttpServletResponse response){
return “create resource (insert data) ok”;
}
/*查询操作/
@PreAuthorize(“hasAuthority(‘sys:res:retrieve’)”)
@RequestMapping(“/doRetrieve”)
public String doRetrieve(){//Retrieve 表示查询
return “query resource (select data) ok”;
}
/*修改操作/
@PreAuthorize(“hasAuthority(‘sys:res:update’)”)
@RequestMapping(“/doUpdate”)
public String doUpdate(){
return “update resource (update data) ok”;
}
/*删除操作/
@PreAuthorize(“hasAuthority(‘sys:res:delete’)”)
@RequestMapping(“/doDelete”)
public String doDelete(){
return “delete resource (dalete data) ok”;
}
}
资源服务器中的资源不是所有人都可以访问的,需要具备一定权限才可以,首先我们要判定是否登陆,然后判定登陆用户是否有权限,有访问权限才可以授权访问,这个操作可以放到spring mvc拦截器中进行实现,例如:
第一步:定义Spring MVC 拦截器.
package sso.resource.interceptor;
public class TokenInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token=request.getHeader(“token”);
if(token==null||“”.equals(token)) throw new RuntimeException(“请先登陆”);
if(JwtUtils.isTokenExpired(token)) throw new RuntimeException(“请先登陆”);
Claims claims=JwtUtils.getClaimsFromToken(token);
List list = (List) claims.get(“authorities”);
String[]authorities=list.toArray(new String[]{});
UserDetails userDetails= User.builder()
.username((String)claims.get(“username”))
.password(“”)
.authorities(authorities)
.build();
//将UserDetails对象保存到一个可以和Spring-Security交互的对象中
PreAuthenticatedAuthenticationToken authenticationToken=
new PreAuthenticatedAuthenticationToken(
userDetails,userDetails.getPassword(),
AuthorityUtils.createAuthorityList(authorities));
//将本次解析的用户详情和当前请求关联
//关联之后才能在后面的控制器中获得用户详情
authenticationToken.setDetails(new WebAuthenticationDetails(request));
//将当前用户详情保存到Spring-Security上下文
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
return true;
}
}
第二步:创建Spring Web配置类,用于注册和配置Spring MVC拦截器,例如:
package sso.resource.config;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new TokenInterceptor())
.addPathPatterns(“/**”);
}
}
第一步:启动认证服务器,通过postman进行登陆认证,例如:
第二步:启动资源服务器,并基于认证服务器返回的令牌进行资源访问
================================================================
当多个项目都有一部分公共资源需要重复编写时,我们可以创建一个公共工程,在这个工程中创建共性对象和依赖.其它工程需要时直接引用即可.
![在这里插入图片描述]( )初始化工程
第一步:添加项目依赖
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-security
io.jsonwebtoken
jjwt
0.9.1
第二步:拷贝工具类
将sso-auth工程中的WebUtils,JwtUtils拷贝到sso-common工程的sso.util包中
在前后端分离工程中,当通过前端工程访问认证服务和资源服务时,需要进行跨域配置,例如:
package sso.config;
@Configuration
public class CorsFilterConfig {
/*服务端过滤器层面的跨域设计/
@Bean
public FilterRegistrationBean filterFilterRegistrationBean(){
//1.对此过滤器进行配置(跨域设置-url,method)
UrlBasedCorsConfigurationSource configSource=new UrlBasedCorsConfigurationSource();
CorsConfiguration config=new CorsConfiguration();
config.addAllowedHeader(“*”);//所有请求头信息
config.addAllowedMethod(“*”);//所有请求方式,post,delete,get,put,…
config.addAllowedOrigin(“*”);//所有请求参数
config.setAllowCredentials(true);//所有认证信息,例如cookie
//2.注册过滤器并设置其优先级
configSource.registerCorsConfiguration(“/**”, config);
FilterRegistrationBean fBean=
new FilterRegistrationBean(
new CorsFilter(configSource));
//设置此过滤器的优先级最高
fBean.setOrder(Ordered.HIGHEST_PRECEDENCE);
return fBean;
}
}
第一步:删除sso-auth,sso-resource 工程下的util包
第二步:删除sso-auth,sso-resource 工程中的公共依赖
第三步:在sso-auth,sso-resource工程中添加通用工程依赖
com.cy.jt
sso-common
1.0-SNAPSHOT
打开postman进行重新登录,访问资源测试.
================================================================
我们做后端,一般在测试时直接基于postman进行访问就可以,为了更好理解前后端通讯过程,我们暂且基于springboot工程构建一个前端工程.
第一步:添加web依赖,代码如下:
org.springframework.boot
spring-boot-starter-web
第二步:创建application.yml配置文件,代码如下:
server:
port: 80
第三步:创建启动类,代码如下:
package sso;
@SpringBootApplication
public class UIApplication {
public static void main(String[] args) {
SpringApplication.run(UIApplication.class,args);
}
}
将课前资料中的static目录直接拷贝到项目中的resource目录下.
登录页面login.html内容如下:
<!doctype html>
Please Login
<button type=“button” @click=“doLogin()” class=“btn btn-primary”>Submit
登录成功页面index.html,代码如下:
Index Page
CRUD(Create,Retrieve,Update,Delete) Operation
- Retrieve(查询-select)
- Delete(删除-delete)
-
启动sso-auth,sso-resource,sso-ui 工程,然后访问http://localhost/login.html进行登录
输入正确的账号,密码执行登录,登录成功以后跳转到如下页面.
然后,对create,update选项进行访问,检测输出结果.
从登录认证,到资源访问,其过程如下:
=================================================================
目前我们登录时的账号,用户权限信息都是写死在UserServiceImpl类中的,实际项目中会从数据库查询用户以及用户对应权限信息.
实际项目中用户权限控制,通常是通过用户,角色,菜单以及他们的关系表进行数据存储,其业务描述如下:
其sql脚本如下:
DROP DATABASE IF EXISTS
jt_security
;CREATE DATABASE
jt_security
DEFAULT CHARACTER SET utf8mb4;SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
use
jt_security
;CREATE TABLE
sys_menu
(id
bigint(20) NOT NULL AUTO_INCREMENT COMMENT ‘ID’,name
varchar(50) NOT NULL COMMENT ‘权限名称’,permission
varchar(200) DEFAULT NULL COMMENT ‘权限标识’,PRIMARY KEY (
id
) USING BTREE) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT=‘权限表’;
CREATE TABLE
sys_role
(id
bigint(11) NOT NULL AUTO_INCREMENT COMMENT ‘角色ID’,role_name
varchar(50) NOT NULL COMMENT ‘角色名称’,PRIMARY KEY (
id
) USING BTREE) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT=‘角色表’;
CREATE TABLE
sys_role_menu
(id
bigint(11) NOT NULL AUTO_INCREMENT COMMENT ‘ID’,role_id
bigint(11) DEFAULT NULL COMMENT ‘角色ID’,menu_id
bigint(11) DEFAULT NULL COMMENT ‘权限ID’,PRIMARY KEY (
id
) USING BTREE) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT=‘角色与权限关系表’;
CREATE TABLE
sys_user
(id
bigint(11) NOT NULL AUTO_INCREMENT COMMENT ‘用户ID’,username
varchar(50) NOT NULL COMMENT ‘用户名’,password
varchar(100) DEFAULT NULL COMMENT ‘密码’,status
varchar(10) DEFAULT NULL COMMENT ‘状态 PROHIBIT:禁用 NORMAL:正常’,PRIMARY KEY (
id
) USING BTREE,UNIQUE KEY
username
(username
) USING BTREE) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT=‘系统用户表’;
CREATE TABLE
sys_user_role
(id
bigint(11) NOT NULL AUTO_INCREMENT COMMENT ‘ID’,user_id
bigint(11) DEFAULT NULL COMMENT ‘用户ID’,role_id
bigint(11) DEFAULT NULL COMMENT ‘角色ID’,PRIMARY KEY (
id
) USING BTREE) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT=‘用户与角色关系表’;
INSERT INTO
sys_menu
VALUES (1, ‘select users’, ‘sys:res:create’);INSERT INTO
sys_menu
VALUES (2, ‘select menus’, ‘sys:res:retrieve’);INSERT INTO
sys_menu
VALUES (3, ‘select roles’, ‘sys:res:delete’);INSERT INTO
sys_role
VALUES (1, ‘ADMIN’);INSERT INTO
sys_role
VALUES (2, ‘USER’);INSERT INTO
sys_role_menu
VALUES (1, 1, 1);INSERT INTO
sys_role_menu
VALUES (2, 1, 2);INSERT INTO
sys_role_menu
VALUES (3, 1, 3);INSERT INTO
sys_role_menu
VALUES (4, 2, 1);INSERT INTO
sys_user
VALUES (1,‘admin’,‘$2a 10 10 10hIAewJVvpTdDSidROQmoXuBBucjLC7sxf7PDMWggZG49cKYhTXt16’,‘NORMAL’);INSERT INTO
sys_user
VALUES (2,‘user’,‘$2a 10 10 10hIAewJVvpTdDSidROQmoXuBBucjLC7sxf7PDMWggZG49cKYhTXt16’,‘NORMAL’);INSERT INTO
sys_user_role
VALUES (1, 1, 1);INSERT INTO
sys_user_role
VALUES (2, 2, 2);
第一步:在sso-auth工程中添加,如下依赖:
org.mybatis.spring.boot
mybatis-spring-boot-starter
2.2.0
mysql
mysql-connector-java
8.0.21
org.springframework.boot
spring-boot-starter-test
test
第二步:修改sso-auth工程中配置文件,添加访问数据库部分,例如:
spring:
datasource:
url: jdbc:mysql:///jt_security?serverTimezone=Asia/Shanghai&characterEncoding=utf8
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver #可以省略,默认会自动识别
第一步:创建UserMapper接口,并定义数据访问方法,代码如下:
package sso.dao;
@Mapper
public interface UserMapper {
/**
-
基于用户查询用户信息
-
@param username
-
@return 查询到的用户信息,表中的字段名会作为map中key,字段名对应的值会
-
作为map中的value进行存储
*/
@Select(“select * from sys_user where username=#{username}”)
Map<String,Object> selectUserByUsername(
@Param(“username”)String username);
/**
- 基于用户id查询用户权限信息
*/
@Select(" select distinct m.permission " +
" from sys_user u left join sys_user_role ur on u.id=ur.user_id " +
" left join sys_role_menu rm on ur.role_id=rm.role_id " +
" left join sys_menu m on rm.menu_id=m.id " +
" where u.id=#{id}")
List selectUserPermissions(@Param(“id”) Long id);
}
第二步:修改UserDetailServiceImpl类添加数据库访问操作,例如:
package sso.service;
/**
- 通过此对象处理登录请求
*/
@Service
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
private BCryptPasswordEncoder passwordEncoder;
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
//1.基于用户名查询用户信息
Map<String,Object> userMap=userMapper.selectUserByUsername(username);
if(userMap==null)throw new UsernameNotFoundException(“user not exists”);
//2.查询用户权限信息并封装查询结果
List userPermissions=
userMapper.selectUserPermissions((Long)userMap.get(“id”));
//权限信息后续要从数据库去查
return new User(username,
(String)userMap.get(“password”),//来自数据库
AuthorityUtils.createAuthorityList(//来自数据库
userPermissions.toArray(new String[]{})));
//这个值返回给谁?谁调用此方法这个就返回给谁.
}
}
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
-