单点登录,英文是 Single Sign On(缩写为 SSO)。即多个站点共用一台认证授权服务器,用户在其中任何一个站点登录后,可以免登录访问其他所有站点。而且,各站点间可以通过该登录状态直接交互。
在这套方案中,用户登录成功后**,会基于UUID生成一个token**,然后与用户信息绑定在一起存储到数据库.后续用户在访问资源时,基于token从数据库查询用户状态,这种方式因为要基于数据库存储和查询用户状态,所以性能表现一般。
在这套方案中,用户登录成功后,会基于JWT技术生成一个token,用户信息可以存储到这个token中.后续用户在访问资源时,对token内容解析,检查登录状态以及权限信息,无须再访问数据库。
服务基于业务划分,系统(system)服务只提供基础数据(例如用户信息、日志信息等),认证服务(auth)负责完成用户身份校验、密码比对,资源服务(resource)代表一些业务服务(例如我的订单、收藏等)。
父工程:02-sso
配置pom文件:
3.3系统基础服务工程设计及实现4.0.0 com.jt 02-sso1.0-SNAPSHOT org.springframework.boot spring-boot-dependencies2.3.2.RELEASE pom import org.springframework.cloud spring-cloud-dependenciesHoxton.SR9 pom import com.alibaba.cloud spring-cloud-alibaba-dependencies2.2.6.RELEASE pom import org.projectlombok lombokprovided org.springframework.boot spring-boot-starter-testtest org.junit.jupiter junit-jupiter-engineorg.apache.maven.plugins maven-compiler-plugin3.8.1 8
本次设计系统服务(System),主要用于提供基础数据服务,例如日志信息,用户信息等
3.3.1数据库表设计 3.3.2创建子工程sso-system 3.3.2.1添加依赖:3.3.2.2在项目中添加bootstrap.yml文件mysql >mysql-connector-javacom.baomidou >mybatis-plus-boot-starter3.4.2 com.alibaba.cloud >spring-cloud-starter-alibaba-nacos-discoverycom.alibaba.cloud >spring-cloud-starter-alibaba-nacos-configorg.springframework.boot >spring-boot-starter-web
server: port: 8061 spring: application: name: sso-system cloud: nacos: discovery: server-addr: localhost:8848 config: server-addr: localhost:8848 file-extension: yml datasource: url: jdbc:mysql:///jt-sso?serverTimezone=Asia/Shanghai&characterEncoding=utf8 username: root password: 123456
可将连接数据库的配置,添加到配置中心
3.3.2.3在项目中添加启动类package com.jt; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class SystemApplication { public static void main(String[] args) { SpringApplication.run(SystemApplication.class,args); } }3.3.2.4在项目中添加单元测试类,测试数据库连接
package com.jt; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import javax.sql.DataSource; import java.sql.Connection; import java.sql.SQLException; @SpringBootTest public class DataSourceTests { @Autowired private DataSource dataSource;//HikariDataSource @Test void testGetConnection() throws SQLException { Connection conn= dataSource.getConnection(); System.out.println(conn); } }3.4POJO对象逻辑实现
添加项目User对象,用于封装用户信息
java中所有用于存储数据的对象,都建议实现序列化接口,并且添加一个序列化id
可参考:String,Integer,ArrayList,HashMap
@Data //编译时有效 @Accessors(chain = true) //@TableName("tb_users"),假如sql语句自己写,不需要此注解指定表名 public class User implements Serializable {//此接口起标识性作用 //序列化id private static final long serialVersionUID = -9218088594214708448L; private Long id; private String username; private String password; private String status; }实现序列化id,alt+enter自动添加UID Service中@Autowired注入UserMapper中对象红色波浪线解决
@Mapper public interface UserMapper extends baseMapper3.6创建UserMapperTests类,对业务方法做单元测试{ @Select("select id,username,password,status from tb_users where username=#{username}") User selectUserByUsername(String username); @Select("select distinct m.permission " + "from tb_user_roles ur join tb_role_menus rm on ur.role_id=rm.role_id " + "join tb_menus m on rm.menu_id=m.id " + "where ur.user_id=#{userId}") List selectUserPermissions(Long userId); }
@SpringBootTest public class UserMapperTests { @Autowired private UserMapper userMapper; @Test void testSelectUserByUsername(){ User user = userMapper.selectUserByUsername("admin"); System.out.println(user); //断言测试,测试结果不正确,就抛异常 //Assert.notNull(user, "user is not exist"); //推荐此种方式 Assertions.assertNotNull(user,"user is not null"); } @Test void testSelectUserPermissions(){ List3.7定义service接口permissions = userMapper.selectUserPermissions(1L); System.out.println(permissions); Assertions.assertNotNull(permissions,"permissions is not null"); } }
public interface UserService { User selectUserByUsername(String username); List3.8定义service接口实现类selectUserPermissions(Long userId); }
@Service public class UserServiceImpl implements UserService { @Autowired private UserMapper userMapper; @Override public User selectUserByUsername(String username) { //Assert.notNull(username,"username can not be null"); //判断是否为空,抛异常 if (StringUtils.isEmpty(username)){ throw new IllegalArgumentException("username can not be null"); } return userMapper.selectUserByUsername(username); } @Cacheable(value = "permissionCache") @Override public ListselectUserPermissions(Long userId) { return userMapper.selectUserPermissions(userId); } }
@EnableCaching //开启spring中的缓存机制,扫描哪个类中用到了缓存
@Cacheable(value = "permissionCache"),此注解描述的方法为缓存切入点方法,从数据库查询到数据后, 可以将数据存储到本地的一个缓存对象中(底层是一个map对象)
@RestController @RequestMapping("/user") public class UserController { @Autowired private UserService userService; @GetMapping("/login/{username}") public User doSelectUserByUsername(@PathVariable String username){ return userService.selectUserByUsername(username); } @GetMapping("/permission/{userId}") public List4.统一认证工程设计及实现doSelectUserPermissions(@PathVariable Long userId){ return userService.selectUserPermissions(userId); } }
用户登陆时调用此工程对用户身份进行统一身份认证和授权。
4.1创建工程sso-auth及初始化 4.1.1编辑pom文件4.1.2在sso-auth工程中创建bootstrap.yml文件org.springframework.boot spring-boot-starter-webcom.alibaba.cloud spring-cloud-starter-alibaba-nacos-discoverycom.alibaba.cloud spring-cloud-starter-alibaba-nacos-configorg.springframework.cloud spring-cloud-starter-oauth2org.springframework.cloud spring-cloud-starter-openfeign
server: port: 8071 spring: application: name: sso-auth cloud: nacos: discovery: server-addr: localhost:8848 config: server-addr: localhost:88484.1.3添加项目启动类
@SpringBootApplication public class AuthApplication { public static void main(String[] args) { SpringApplication.run(AuthApplication.class, args); } }4.1.4启动并访问项目
项目启动时,系统会默认生成一个登陆密码
打开浏览器输入http://localhost:8071呈现登录页面
默认用户名为user,密码为系统启动时,在控制台呈现的密码。执行登陆测试,登陆成功进入如下界面(因为没有定义登录页面,所以会出现404)。
第一步:定义User对象,用于封装从数据库查询到的用户信息
package com.jt.auth.pojo; import lombok.Data; import java.io.Serializable; @Data public class User implements Serializable { private static final long serialVersionUID = 4831304712151465443L; private Long id; private String username; private String password; private String status; }
第二步:定义用户远程调用Feign接口RemoteUserService ,基于此接口调用sso-system服务中的用户信息
@FeignClient(value = "sso-system", contextId = "remoteUserService") public interface RemoteUserService { @GetMapping("/user/login/{username}") User selectUserByUsername(@PathVariable(name = "username") String username); @GetMapping("/user/permission/{userId}") ListselectUserPermissions(@PathVariable("userId") Long userId); }
第三步:定义远程Service对象,用于实现远程用户信息调用
@Slf4j @Service public class UserDetailServiceImpl implements UserDetailsService { @Autowired private RemoteUserService remoteUserService; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //1.基于用户名获取用户信息(远程Feign方式服务调用) com.jt.auth.pojo.User user = remoteUserService.selectUserByUsername(username); if(user == null) throw new UsernameNotFoundException("user id not exist"); //2.基于用户id获取用户权限信息 List5.定义Security配置类permissions = remoteUserService.selectUserPermissions(user.getId()); log.debug("permissions {}",permissions); //3.封装用户信息并返回 User userDetails = new User(username, user.getPassword(), AuthorityUtils.createAuthorityList(permissions.toArray(new String[]{}))); return userDetails; //交给spring security的认证中心,进行认证分析(比对) } }
在此类中配置认证规则
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Override protected void configure(HttpSecurity http) throws Exception { //super.configure(http); //禁用跨域攻击,假如没有禁用,使用postman,httpclient这些工具登录失败403 //http.csrf().disable(); //所有**资源必须认证才能访问,403没有权限 //http.authorizeRequests().antMatchers(" //登录配置,去哪里认证,认证成功或失败的处理器是谁 // http.formLogin().defaultSuccessUrl("index.html"); //redirect:index.html重定向 // http.formLogin().successForwardUrl("/doIndex"); //前后端分离的写法,登录成功要返回json字符串 http.formLogin() .successHandler(successHandler()) .failureHandler(failureHandler()); } //定义认证成功处理器 //登录成功以后返回json数据 @Bean public AuthenticationSuccessHandler successHandler() { return new AuthenticationSuccessHandler() { @Override public void onAuthenticationSuccess( HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { Map基于浏览器进行访问测试map = new HashMap<>(); map.put("status", 200); map.put("message", "login success"); writeJsonToClient(response,map); } }; } //lambda public AuthenticationFailureHandler failureHandler() { return (request, response, authentication) ->{ Map map = new HashMap<>(); map.put("status", 201); map.put("message", "login failure"); writeJsonToClient(response,map); }; } private void writeJsonToClient(HttpServletResponse response,Map map) throws IOException { //将map对象,转换为json String jsonStr =new ObjectMapper().writevalueAsString(map); //设置响应数据的编码方式 response.setCharacterEncoding("utf-8"); //设置响应数据的类型 response.setContentType("application/json;charset:utf8"); //将数据响应到客户端 PrintWriter out=response.getWriter(); out.println(jsonStr); out.flush(); } }
启动sso-system,sso-auth服务,然后基于浏览器访问网关,执行登录测试,admin,123456
目前的登陆 *** 作,也就是用户的认证 *** 作,其实现主要基于Spring Security框架,其认证简易流程如下
Oauth2是一种协议或规范,定义了完成用户身份认证和授权的方式,
比如:基于密码身份认证,基于指纹,基于第三方令牌认证(QQ,微信登录),但具体完成过程需要一组对象,这些对象构成,有如下几个部分:
0.系统数据资源(类似数据库,文件系统)
1.资源服务器(负责访问资源,例如:商品,订单,库存,会员…)
2.认证服务器(负责完成用户身份的认证)
3.客户端对象(表单,令牌,)
4.资源拥有者(用户)在Oauth2这中规范下,如何对用户什么进行认证?
1.认证的地址(让用户去哪里认证)
2.用户需要携带什么信息去认证(办理)
3.具体完成认证的对象是谁
在Security配置类添加以下方法,在Oauth2Config注入此对象:
//方法返回的对象为后续的oauth2的配置提供服务 @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); }
定义Oauth2Config配置类:
@Configuration @EnableAuthorizationServer public class Oauth2Config extends AuthorizationServerConfigurerAdapter { @Autowired private PasswordEncoder passwordEncoder; @Autowired private AuthenticationManager authenticationManager; //提供一个认证的入口(客户端去哪里认证)?(http://ip:port/.....) @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { //super.configure(security); //公开认证地址(/auth/token) security.tokenKeyAccess("permitAll()") //公开检查令牌的入口(/oauth/check_token) .checkTokenAccess("permitAll()") //允许通过表单方式进行认证 .allowFormAuthenticationForClients(); } @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { //super.configure(clients); clients.inMemory() //客户端标识 .withClient("gateway-client") //客户端携带密钥 123456 .secret(passwordEncoder.encode("123456")) //定义认证类型(允许对哪些数据进行认证) .authorizedGrantTypes("password","refresh_token") //作用域,满足如上条件的所有客户端可以来这里进行认证 .scopes("all"); } @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { //super.configure(endpoints); //定义由谁完成认证 endpoints.authenticationManager(authenticationManager); } }6.2构建令牌生成及配置对象
本次我们借助JWT(Json Web Token-是一种json格式)方式将用户相关信息进行组织和加密,并作为响应令牌(Token),从服务端响应到客户端,客户端接收到这个JWT令牌之后,将其保存在客户端(例如localStorage),然后携带令牌访问资源服务器,资源服务器获取并解析令牌的合法性,基于解析结果判定是否允许用户访问资源。
@Configuration public class TokenConfig { @Bean public TokenStore tokenStore() { return new JwtTokenStore(jwtAccessTokenConverter()); } @Bean public JwtAccessTokenConverter jwtAccessTokenConverter(){ JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter(); //设置签名key,对Jwt令牌进行签名时使用,key不能让客户端知道 jwtAccessTokenConverter.setSigningKey(signingKey); return jwtAccessTokenConverter; } private String signingKey="auth"; }6.3启动postman进行访问测试
检查token信息:
资源服务工程为一个业务数据工程,此工程中数据在访问通常情况下是受限访问,例如有些资源有用户,都可以方法,有些资源必须认证才可访问,有些资源认证后,有权限才可以访问。
7.1业务设计架构用户访问资源时的认证,授权流程设计如下:
7.2.2第二步:创建bootstrap.yml配置文件:org.springframework.boot spring-boot-starter-webcom.alibaba.cloud spring-cloud-starter-alibaba-nacos-discoverycom.alibaba.cloud spring-cloud-starter-alibaba-nacos-configcom.alibaba.cloud spring-cloud-starter-alibaba-sentinelorg.springframework.cloud spring-cloud-starter-oauth2
server: port: 8881 spring: application: name: sso-resource cloud: nacos: discovery: server-addr: localhost:8848 config: server-addr: localhost:8848 file-extension: yml7.2.3第三步:创建启动类
package com.jt; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class ResourceApplication { public static void main(String[] args) { SpringApplication.run(ResourceApplication.class,args); } }7.2.4第四步:创建资源Controller对象
@RestController @RequestMapping("/resource") public class ResourceController { @PreAuthorize("hasAuthority('sys:res:list')")//描述的是一个切入点方法,访问此方法,需加此权限 @GetMapping public String doSelect(){ return "Select Resource ok"; } @PreAuthorize("hasAuthority('sys:res:create')") @PostMapping public String doCreate(){ return "Create Resource OK"; } @PreAuthorize("hasAuthority('sys:res:update')") @PutMapping public String doUpdate(){ return "Update Resource OK"; } @DeleteMapping public String doDelete(){ return "Delete resource ok"; } }7.2.5第五步:配置资源认证授权规则ResourceConfig
@Configuration @EnableResourceServer //启动资源服务器的默认配置 //启动方法上的权限控制,需要授权才可访问的方法上添加@PreAuthorize等相关注解 @EnableGlobalMethodSecurity(prePostEnabled = true) public class ResourceConfig extends ResourceServerConfigurerAdapter { @Override public void configure(HttpSecurity http) throws Exception { //super.configure(http); //1.关闭跨域攻击 http.csrf().disable(); //2.放行相关请求 http.authorizeRequests() //配置炫耀认证的资源 .antMatchers("/resource @Configuration public class TokenConfig { @Bean public TokenStore tokenStore(){ //这里采用JWT方式生成和存储令牌信息 return new JwtTokenStore(jwtAccessTokenConverter()); } @Bean public JwtAccessTokenConverter jwtAccessTokenConverter(){ JwtAccessTokenConverter jwtAccessTokenConverter= new JwtAccessTokenConverter(); //JWT令牌构成:header(签名算法,令牌类型),payload(数据部分),Signing(签名) //这里的签名可以简单理解为加密,加密时会使用header中算法以及我们自己提供的密钥, //这里加密的目的是为了防止令牌被篡改。(这里密钥要保管好,要存储在服务端) jwtAccessTokenConverter.setSigningKey(SIGNING_KEY);//设置密钥 return jwtAccessTokenConverter; } private static final String SIGNING_KEY="auth"; }7.2.7第七步:启动Postman进行访问测试:
不携带令牌访问:
携带令牌访问:
没有访问权限:
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)